feed period select (#340)

* feed period select

* fix, unused code removed

* Fix styles

* Fix styles

* Fix styles

---------

Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
Co-authored-by: ilya-bkv <i.yablokov@ccmp.me>
This commit is contained in:
Igor Lobanov 2023-12-20 09:07:57 +01:00 committed by GitHub
parent 88d35ce2bc
commit 3a7d138eef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 260 additions and 92 deletions

View File

@ -357,9 +357,12 @@
"This comment has not yet been rated": "This comment has not yet been rated", "This comment has not yet been rated": "This comment has not yet been rated",
"This email is already taken. If it's you": "This email is already taken. If it's you", "This email is already taken. If it's you": "This email is already taken. If it's you",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.", "This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.",
"This month": "This month",
"This post has not been rated yet": "This post has not been rated yet", "This post has not been rated yet": "This post has not been rated yet",
"This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted": "This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted", "This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted": "This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted",
"This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed": "This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed", "This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed": "This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed",
"This week": "This week",
"This year": "This year",
"To leave a comment please": "To leave a comment please", "To leave a comment please": "To leave a comment please",
"To write a comment, you must": "To write a comment, you must", "To write a comment, you must": "To write a comment, you must",
"Top authors": "Authors rating", "Top authors": "Authors rating",

View File

@ -377,9 +377,12 @@
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы", "This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.", "This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
"This month": "За месяц",
"This post has not been rated yet": "Эту публикацию еще пока никто не оценил", "This post has not been rated yet": "Эту публикацию еще пока никто не оценил",
"This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted": "Так мы&nbsp;поймем, что вы&nbsp;реальный человек, и&nbsp;учтем ваш голос. А&nbsp;вы&nbsp;увидите, как проголосовали другие", "This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted": "Так мы&nbsp;поймем, что вы&nbsp;реальный человек, и&nbsp;учтем ваш голос. А&nbsp;вы&nbsp;увидите, как проголосовали другие",
"This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed": "Так вы&nbsp;сможете подписаться на&nbsp;авторов, интересные темы и&nbsp;настроить свою ленту", "This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed": "Так вы&nbsp;сможете подписаться на&nbsp;авторов, интересные темы и&nbsp;настроить свою ленту",
"This week": "За неделю",
"This year": "За год",
"To leave a comment please": "Чтобы оставить комментарий, необходимо", "To leave a comment please": "Чтобы оставить комментарий, необходимо",
"To write a comment, you must": "Чтобы написать комментарий, необходимо", "To write a comment, you must": "Чтобы написать комментарий, необходимо",
"Top authors": "Рейтинг авторов", "Top authors": "Рейтинг авторов",

View File

@ -135,7 +135,7 @@ export const FullArticle = (props: Props) => {
scrollTo(commentsRef.current) scrollTo(commentsRef.current)
} }
const { searchParams, changeSearchParam } = useRouter<ArticlePageSearchParams>() const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
createEffect(() => { createEffect(() => {
if (props.scrollToComments) { if (props.scrollToComments) {
@ -146,7 +146,7 @@ export const FullArticle = (props: Props) => {
createEffect(() => { createEffect(() => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments() scrollToComments()
changeSearchParam({ changeSearchParams({
scrollTo: null, scrollTo: null,
}) })
} }
@ -158,7 +158,7 @@ export const FullArticle = (props: Props) => {
`[id='comment_${searchParams().commentId}']`, `[id='comment_${searchParams().commentId}']`,
) )
changeSearchParam({ commentId: null }) changeSearchParams({ commentId: null })
if (commentElement) { if (commentElement) {
scrollTo(commentElement) scrollTo(commentElement)

View File

@ -13,7 +13,7 @@ type Props = {
export const AuthGuard = (props: Props) => { export const AuthGuard = (props: Props) => {
const { isAuthenticated, isSessionLoaded } = useSession() const { isAuthenticated, isSessionLoaded } = useSession()
const { changeSearchParam } = useRouter<RootSearchParams & AuthModalSearchParams>() const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
createEffect(() => { createEffect(() => {
if (props.disabled) { if (props.disabled) {
@ -23,7 +23,7 @@ export const AuthGuard = (props: Props) => {
if (isAuthenticated()) { if (isAuthenticated()) {
hideModal() hideModal()
} else { } else {
changeSearchParam( changeSearchParams(
{ {
source: 'authguard', source: 'authguard',
modal: 'auth', modal: 'auth',

View File

@ -29,7 +29,7 @@ export const AuthorBadge = (props: Props) => {
subscriptions, subscriptions,
actions: { loadSubscriptions, requireAuthentication }, actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const { changeSearchParam } = useRouter() const { changeSearchParams } = useRouter()
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const subscribed = createMemo(() => const subscribed = createMemo(() =>
subscriptions().authors.some((author) => author.slug === props.author.slug), subscriptions().authors.some((author) => author.slug === props.author.slug),
@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => {
const initChat = () => { const initChat = () => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParam({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author.id.toString(),
}) })
}, 'discussions') }, 'discussions')

View File

@ -72,11 +72,11 @@ export const AuthorCard = (props: Props) => {
}) })
// TODO: reimplement AuthorCard // TODO: reimplement AuthorCard
const { changeSearchParam } = useRouter() const { changeSearchParams } = useRouter()
const initChat = () => { const initChat = () => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParam({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author.id.toString(),
}) })
}, 'discussions') }, 'discussions')

View File

@ -7,7 +7,7 @@ import styles from './Hero.module.scss'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
return ( return (
<div class={styles.aboutDiscours}> <div class={styles.aboutDiscours}>
@ -28,7 +28,7 @@ export default () => {
class="button" class="button"
onClick={() => { onClick={() => {
showModal('auth') showModal('auth')
changeSearchParam({ changeSearchParams({
mode: 'register', mode: 'register',
}) })
}} }}

View File

@ -98,11 +98,11 @@ export const ArticleCard = (props: ArticleCardProps) => {
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
const { changeSearchParam } = useRouter() const { changeSearchParams } = useRouter()
const scrollToComments = (event) => { const scrollToComments = (event) => {
event.preventDefault() event.preventDefault()
openPage(router, 'article', { slug: props.article.slug }) openPage(router, 'article', { slug: props.article.slug })
changeSearchParam({ changeSearchParams({
scrollTo: 'comments', scrollTo: 'comments',
}) })
} }

View File

@ -20,7 +20,7 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>> type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const ForgotPasswordForm = () => { export const ForgotPasswordForm = () => {
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const handleEmailInput = (newEmail: string) => { const handleEmailInput = (newEmail: string) => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
@ -119,7 +119,7 @@ export const ForgotPasswordForm = () => {
href="#" href="#"
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
changeSearchParam({ changeSearchParams({
mode: 'register', mode: 'register',
}) })
}} }}
@ -141,7 +141,7 @@ export const ForgotPasswordForm = () => {
<span <span
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParams({
mode: 'login', mode: 'login',
}) })
} }

View File

@ -47,7 +47,7 @@ export const LoginForm = () => {
actions: { signIn }, actions: { signIn },
} = useSession() } = useSession()
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')
@ -201,7 +201,7 @@ export const LoginForm = () => {
<span <span
class="link" class="link"
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParams({
mode: 'forgot-password', mode: 'forgot-password',
}) })
} }
@ -218,7 +218,7 @@ export const LoginForm = () => {
<span <span
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParams({
mode: 'register', mode: 'register',
}) })
} }

View File

@ -32,7 +32,7 @@ const handleEmailInput = (newEmail: string) => {
} }
export const RegisterForm = () => { export const RegisterForm = () => {
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { emailChecks } = useEmailChecks() const { emailChecks } = useEmailChecks()
@ -202,7 +202,7 @@ export const RegisterForm = () => {
href="#" href="#"
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
changeSearchParam({ changeSearchParams({
mode: 'login', mode: 'login',
}) })
}} }}
@ -255,7 +255,7 @@ export const RegisterForm = () => {
<span <span
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParams({
mode: 'login', mode: 'login',
}) })
} }

View File

@ -42,7 +42,7 @@ export const NotificationView = (props: Props) => {
actions: { markNotificationAsRead, hideNotificationsPanel }, actions: { markNotificationAsRead, hideNotificationsPanel },
} = useNotifications() } = useNotifications()
const { changeSearchParam } = useRouter<ArticlePageSearchParams>() const { changeSearchParams } = useRouter<ArticlePageSearchParams>()
const { t, formatDate, formatTime } = useLocalize() const { t, formatDate, formatTime } = useLocalize()
@ -139,7 +139,7 @@ export const NotificationView = (props: Props) => {
openPage(router, 'article', { slug: data().shout.slug }) openPage(router, 'article', { slug: data().shout.slug })
if (data().reactionIds) { if (data().reactionIds) {
changeSearchParam({ commentId: data().reactionIds[0].toString() }) changeSearchParams({ commentId: data().reactionIds[0].toString() })
} }
} }

View File

@ -31,7 +31,7 @@ const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫ
export const AllAuthorsView = (props: Props) => { export const AllAuthorsView = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const [limit, setLimit] = createSignal(PAGE_SIZE) const [limit, setLimit] = createSignal(PAGE_SIZE)
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>() const { searchParams, changeSearchParams } = useRouter<AllAuthorsPageSearchParams>()
const { sortedAuthors } = useAuthorsStore({ const { sortedAuthors } = useAuthorsStore({
authors: props.authors, authors: props.authors,
sortBy: searchParams().by || 'shouts', sortBy: searchParams().by || 'shouts',
@ -41,7 +41,7 @@ export const AllAuthorsView = (props: Props) => {
createEffect(() => { createEffect(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParam({ changeSearchParams({
by: 'shouts', by: 'shouts',
}) })
} }

View File

@ -31,7 +31,7 @@ const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫ
export const AllTopicsView = (props: Props) => { export const AllTopicsView = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>() const { searchParams, changeSearchParams } = useRouter<AllTopicsPageSearchParams>()
const [limit, setLimit] = createSignal(PAGE_SIZE) const [limit, setLimit] = createSignal(PAGE_SIZE)
const { sortedTopics } = useTopicsStore({ const { sortedTopics } = useTopicsStore({
@ -43,7 +43,7 @@ export const AllTopicsView = (props: Props) => {
createEffect(() => { createEffect(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParam({ changeSearchParams({
by: 'shouts', by: 'shouts',
}) })
} }

View File

@ -46,7 +46,7 @@ export const Expo = (props: Props) => {
}) })
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => { const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
const filters = { ...additionalFilters } const filters = { visibility: 'public', ...additionalFilters }
if (props.layout) { if (props.layout) {
filters.layout = props.layout filters.layout = props.layout

View File

@ -1,7 +1,4 @@
.feedFilter { .feedFilter {
margin-bottom: 4.8rem;
margin-top: 0.2em;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
margin-right: 4rem !important; margin-right: 4rem !important;
} }
@ -192,3 +189,25 @@
font-weight: 500; font-weight: 500;
} }
.filtersContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4rem;
.feedFilter {
margin-top: 0;
margin-bottom: 0;
& > li {
margin-bottom: 0;
}
}
}
.periodSwitcher {
font-size: 14px;
font-weight: 700;
line-height: 18px;
}

View File

@ -3,7 +3,7 @@ import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphq
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, on, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions' import { useReactions } from '../../../context/reactions'
@ -13,6 +13,8 @@ import { useTopAuthorsStore } from '../../../stores/zine/topAuthors'
import { useTopicsStore } from '../../../stores/zine/topics' import { useTopicsStore } from '../../../stores/zine/topics'
import { apiClient } from '../../../utils/apiClient' import { apiClient } from '../../../utils/apiClient'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { getServerDate } from '../../../utils/getServerDate'
import { DropDown } from '../../_shared/DropDown'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
import { CommentDate } from '../../Article/CommentDate' import { CommentDate } from '../../Article/CommentDate'
@ -28,8 +30,16 @@ import stylesTopic from '../../Feed/CardTopic.module.scss'
export const FEED_PAGE_SIZE = 20 export const FEED_PAGE_SIZE = 20
const UNRATED_ARTICLES_COUNT = 5 const UNRATED_ARTICLES_COUNT = 5
type FeedPeriod = 'week' | 'month' | 'year'
type PeriodItem = {
value: FeedPeriod
title: string
}
type FeedSearchParams = { type FeedSearchParams = {
by: 'publish_date' | 'rating' | 'last_comment' by: 'publish_date' | 'rating' | 'last_comment'
period: FeedPeriod
} }
const getOrderBy = (by: FeedSearchParams['by']) => { const getOrderBy = (by: FeedSearchParams['by']) => {
@ -53,7 +63,16 @@ type Props = {
export const Feed = (props: Props) => { export const Feed = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { page, searchParams } = useRouter<FeedSearchParams>()
const monthPeriod: PeriodItem = { value: 'month', title: t('This month') }
const periods: PeriodItem[] = [
{ value: 'week', title: t('This week') },
monthPeriod,
{ value: 'year', title: t('This year') },
]
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false) const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
@ -64,6 +83,16 @@ export const Feed = (props: Props) => {
const [topComments, setTopComments] = createSignal<Reaction[]>([]) const [topComments, setTopComments] = createSignal<Reaction[]>([])
const [unratedArticles, setUnratedArticles] = createSignal<Shout[]>([]) const [unratedArticles, setUnratedArticles] = createSignal<Shout[]>([])
const currentPeriod = createMemo(() => {
const period = periods.find((p) => p.value === searchParams().period)
if (!period) {
return monthPeriod
}
return period
})
const { const {
actions: { loadReactionsBy }, actions: { loadReactionsBy },
} = useReactions() } = useReactions()
@ -86,7 +115,7 @@ export const Feed = (props: Props) => {
createEffect( createEffect(
on( on(
() => page().route + searchParams().by, () => page().route + searchParams().by + searchParams().period,
() => { () => {
resetSortedArticles() resetSortedArticles()
loadMore() loadMore()
@ -94,6 +123,21 @@ export const Feed = (props: Props) => {
{ defer: true }, { defer: true },
), ),
) )
const getFromDate = (period: FeedPeriod): Date => {
const now = new Date()
switch (period) {
case 'week': {
return new Date(now.setDate(now.getDate() - 7))
}
case 'month': {
return new Date(now.setMonth(now.getMonth() - 1))
}
case 'year': {
return new Date(now.setFullYear(now.getFullYear() - 1))
}
}
}
const loadFeedShouts = () => { const loadFeedShouts = () => {
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
limit: FEED_PAGE_SIZE, limit: FEED_PAGE_SIZE,
@ -106,6 +150,12 @@ export const Feed = (props: Props) => {
options.order_by = orderBy options.order_by = orderBy
} }
if (searchParams().by && searchParams().by !== 'publish_date') {
const period = searchParams().period || 'month'
const fromDate = getFromDate(period)
options.filters = { fromDate: getServerDate(fromDate) }
}
return props.loadShouts(options) return props.loadShouts(options)
} }
@ -148,32 +198,49 @@ export const Feed = (props: Props) => {
</div> </div>
<div class="col-md-12 offset-xl-1"> <div class="col-md-12 offset-xl-1">
<ul class={clsx(styles.feedFilter, 'view-switcher')}> <div class={styles.filtersContainer}>
<li <ul class={clsx('view-switcher', styles.feedFilter)}>
class={clsx({ <li
'view-switcher__item--selected': searchParams().by === 'publish_date' || !searchParams().by, class={clsx({
})} 'view-switcher__item--selected':
> searchParams().by === 'publish_date' || !searchParams().by,
<a href={getPagePath(router, page().route)}>{t('Recent')}</a> })}
</li> >
{/*<li>*/} <a href={getPagePath(router, page().route)}>{t('Recent')}</a>
{/* <a href="/feed/?by=views">{t('Most read')}</a>*/} </li>
{/*</li>*/} {/*<li>*/}
<li {/* <a href="/feed/?by=views">{t('Most read')}</a>*/}
class={clsx({ {/*</li>*/}
'view-switcher__item--selected': searchParams().by === 'rating', <li
})} class={clsx({
> 'view-switcher__item--selected': searchParams().by === 'rating',
<a href={`${getPagePath(router, page().route)}?by=rating`}>{t('Top rated')}</a> })}
</li> >
<li <span class="link" onClick={() => changeSearchParams({ by: 'rating' })}>
class={clsx({ {t('Top rated')}
'view-switcher__item--selected': searchParams().by === 'last_comment', </span>
})} </li>
> <li
<a href={`${getPagePath(router, page().route)}?by=last_comment`}>{t('Most commented')}</a> class={clsx({
</li> 'view-switcher__item--selected': searchParams().by === 'last_comment',
</ul> })}
>
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
{t('Most commented')}
</span>
</li>
</ul>
<Show when={searchParams().by && searchParams().by !== 'publish_date'}>
<div>
<DropDown
options={periods}
currentOption={currentPeriod()}
triggerCssClass={styles.periodSwitcher}
onChange={(period) => changeSearchParams({ period: period.value })}
/>
</div>
</Show>
</div>
<Show when={!isLoading()} fallback={<Loading />}> <Show when={!isLoading()} fallback={<Loading />}>
<Show when={sortedArticles().length > 0}> <Show when={sortedArticles().length > 0}>

View File

@ -115,9 +115,7 @@ export const HomeView = (props: Props) => {
wrapper={'top-article'} wrapper={'top-article'}
nodate={true} nodate={true}
/> />
<Row3 articles={sortedArticles().slice(6, 9)} nodate={true} /> <Row3 articles={sortedArticles().slice(6, 9)} nodate={true} />
<Beside <Beside
beside={sortedArticles()[9]} beside={sortedArticles()[9]}
title={t('Top authors')} title={t('Top authors')}
@ -125,15 +123,11 @@ export const HomeView = (props: Props) => {
wrapper={'author'} wrapper={'author'}
nodate={true} nodate={true}
/> />
<Show when={topMonthArticles()}> <Show when={topMonthArticles()}>
<ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} /> <ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
</Show> </Show>
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} /> <Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
<RowShort articles={sortedArticles().slice(12, 16)} /> <RowShort articles={sortedArticles().slice(12, 16)} />
<Row1 article={sortedArticles()[16]} nodate={true} /> <Row1 article={sortedArticles()[16]} nodate={true} />
<Row3 articles={sortedArticles().slice(17, 20)} nodate={true} /> <Row3 articles={sortedArticles().slice(17, 20)} nodate={true} />
<Row3 <Row3
@ -141,13 +135,10 @@ export const HomeView = (props: Props) => {
header={<h2>{t('Top commented')}</h2>} header={<h2>{t('Top commented')}</h2>}
nodate={true} nodate={true}
/> />
{randomLayout()} {randomLayout()}
<Show when={topArticles()}> <Show when={topArticles()}>
<ArticleCardSwiper title={t('Favorite')} slides={topArticles()} /> <ArticleCardSwiper title={t('Favorite')} slides={topArticles()} />
</Show> </Show>
<Beside <Beside
beside={sortedArticles()[20]} beside={sortedArticles()[20]}
title={t('Top topics')} title={t('Top topics')}
@ -156,11 +147,8 @@ export const HomeView = (props: Props) => {
isTopicCompact={true} isTopicCompact={true}
nodate={true} nodate={true}
/> />
<Row3 articles={sortedArticles().slice(21, 24)} nodate={true} /> <Row3 articles={sortedArticles().slice(21, 24)} nodate={true} />
<Banner /> <Banner />
<Row2 articles={sortedArticles().slice(24, 26)} nodate={true} /> <Row2 articles={sortedArticles().slice(24, 26)} nodate={true} />
<Row3 articles={sortedArticles().slice(26, 29)} nodate={true} /> <Row3 articles={sortedArticles().slice(26, 29)} nodate={true} />
<Row2 articles={sortedArticles().slice(29, 31)} nodate={true} /> <Row2 articles={sortedArticles().slice(29, 31)} nodate={true} />

View File

@ -52,7 +52,7 @@ export const InboxView = () => {
const [isClear, setClear] = createSignal(false) const [isClear, setClear] = createSignal(false)
const { session } = useSession() const { session } = useSession()
const currentUserId = createMemo(() => session()?.user.id) const currentUserId = createMemo(() => session()?.user.id)
const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>() const { changeSearchParams, searchParams } = useRouter<InboxSearchParams>()
// Поиск по диалогам // Поиск по диалогам
const getQuery = (query) => { const getQuery = (query) => {
if (query().length >= 2) { if (query().length >= 2) {
@ -67,7 +67,7 @@ export const InboxView = () => {
const handleOpenChat = async (chat: Chat) => { const handleOpenChat = async (chat: Chat) => {
setCurrentDialog(chat) setCurrentDialog(chat)
changeSearchParam({ changeSearchParams({
chat: chat.id, chat: chat.id,
}) })
try { try {
@ -126,7 +126,7 @@ export const InboxView = () => {
try { try {
const newChat = await createChat([Number(searchParams().initChat)], '') const newChat = await createChat([Number(searchParams().initChat)], '')
await loadChats() await loadChats()
changeSearchParam({ changeSearchParams({
initChat: null, initChat: null,
chat: newChat.chat.id, chat: newChat.chat.id,
}) })

View File

@ -41,7 +41,7 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
export const TopicView = (props: Props) => { export const TopicView = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>() const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
@ -125,7 +125,7 @@ export const TopicView = (props: Props) => {
<button <button
type="button" type="button"
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParams({
by: 'recent', by: 'recent',
}) })
} }
@ -135,17 +135,17 @@ export const TopicView = (props: Props) => {
</li> </li>
{/*TODO: server sort*/} {/*TODO: server sort*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'rating' }}>*/} {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'rating' }}>*/}
{/* <button type="button" onClick={() => changeSearchParam('by', 'rating')}>*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'rating')}>*/}
{/* {t('Popular')}*/} {/* {t('Popular')}*/}
{/* </button>*/} {/* </button>*/}
{/*</li>*/} {/*</li>*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'viewed' }}>*/} {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'viewed' }}>*/}
{/* <button type="button" onClick={() => changeSearchParam('by', 'viewed')}>*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'viewed')}>*/}
{/* {t('Views')}*/} {/* {t('Views')}*/}
{/* </button>*/} {/* </button>*/}
{/*</li>*/} {/*</li>*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'commented' }}>*/} {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'commented' }}>*/}
{/* <button type="button" onClick={() => changeSearchParam('by', 'commented')}>*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'commented')}>*/}
{/* {t('Discussing')}*/} {/* {t('Discussing')}*/}
{/* </button>*/} {/* </button>*/}
{/*</li>*/} {/*</li>*/}

View File

@ -0,0 +1,7 @@
.chevron {
vertical-align: top;
&.rotate {
transform: rotate(180deg);
}
}

View File

@ -0,0 +1,69 @@
import type { PopupProps } from '../Popup'
import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js'
import { Popup } from '../Popup'
import styles from './DropDown.module.scss'
export type Option = {
value: string | number
title: string
}
type Props<TOption> = {
class?: string
popupProps?: PopupProps
options: TOption[]
currentOption: TOption
triggerCssClass?: string
onChange: (option: TOption) => void
}
const Chevron = (props: { class?: string }) => {
return (
<svg
class={props.class}
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path d="M13.5 6L9 12L4.5 6H13.5Z" fill="#141414" />
</svg>
)
}
export const DropDown = <TOption extends Option = Option>(props: Props<TOption>) => {
const [isPopupVisible, setIsPopupVisible] = createSignal(false)
return (
<Show when={props.currentOption} keyed={true}>
<Popup
trigger={
<div class={props.triggerCssClass}>
{props.currentOption.title}{' '}
<Chevron
class={clsx(styles.chevron, {
[styles.rotate]: isPopupVisible(),
})}
/>
</div>
}
variant="tiny"
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
{...props.popupProps}
>
<For each={props.options.filter((p) => p.value !== props.currentOption.value)}>
{(option) => (
<div class="link" onClick={() => props.onChange(option)}>
{option.title}
</div>
)}
</For>
</Popup>
</Show>
)
}

View File

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

View File

@ -32,7 +32,9 @@ export const Popup = (props: PopupProps) => {
useOutsideClickHandler({ useOutsideClickHandler({
containerRef, containerRef,
predicate: () => isVisible(), predicate: () => isVisible(),
handler: () => setIsVisible(false), handler: () => {
setIsVisible(false)
},
}) })
const toggle = () => setIsVisible((oldVisible) => !oldVisible) const toggle = () => setIsVisible((oldVisible) => !oldVisible)

View File

@ -32,7 +32,7 @@ export function useLocalize() {
export const LocalizeProvider = (props: { children: JSX.Element }) => { export const LocalizeProvider = (props: { children: JSX.Element }) => {
const [lang, setLang] = createSignal<Language>(i18next.language === 'en' ? 'en' : 'ru') const [lang, setLang] = createSignal<Language>(i18next.language === 'en' ? 'en' : 'ru')
const { searchParams, changeSearchParam } = useRouter<{ const { searchParams, changeSearchParams } = useRouter<{
lng: string lng: string
}>() }>()
@ -46,7 +46,7 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => {
changeLanguage(lng) changeLanguage(lng)
setLang(lng) setLang(lng)
Cookie.set('lng', lng) Cookie.set('lng', lng)
changeSearchParam({ lng: null }, true) changeSearchParams({ lng: null }, true)
}) })
const formatTime = (date: Date, options: Intl.DateTimeFormatOptions = {}) => { const formatTime = (date: Date, options: Intl.DateTimeFormatOptions = {}) => {

View File

@ -12,7 +12,10 @@ import { loadMyFeed, loadShouts, resetSortedArticles } from '../stores/zine/arti
const handleFeedLoadShouts = (options: LoadShoutsOptions) => { const handleFeedLoadShouts = (options: LoadShoutsOptions) => {
return loadShouts({ return loadShouts({
...options, ...options,
filters: { visibility: 'community' }, filters: {
visibility: 'community',
...options.filters,
},
}) })
} }

View File

@ -138,7 +138,7 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
const page = useStore(routerStore) const page = useStore(routerStore)
const searchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams> const searchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
const changeSearchParam = (newValues: Partial<TSearchParams>, replace = false) => { const changeSearchParams = (newValues: Partial<TSearchParams>, replace = false) => {
const newSearchParams = { ...searchParamsStore.get() } const newSearchParams = { ...searchParamsStore.get() }
Object.keys(newValues).forEach((key) => { Object.keys(newValues).forEach((key) => {
@ -155,6 +155,6 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
return { return {
page, page,
searchParams, searchParams,
changeSearchParam, changeSearchParams,
} }
} }

View File

@ -42,13 +42,13 @@ export const MODALS: Record<ModalType, ModalType> = {
const [modal, setModal] = createSignal<ModalType>(null) const [modal, setModal] = createSignal<ModalType>(null)
const { searchParams, changeSearchParam } = useRouter< const { searchParams, changeSearchParams } = useRouter<
AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams
>() >()
export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) => { export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) => {
if (modalSource) { if (modalSource) {
changeSearchParam({ changeSearchParams({
source: modalSource, source: modalSource,
}) })
} }
@ -70,7 +70,7 @@ export const hideModal = () => {
newSearchParams.mode = null newSearchParams.mode = null
} }
changeSearchParam(newSearchParams, true) changeSearchParams(newSearchParams, true)
setModal(null) setModal(null)
} }

View File

@ -204,7 +204,7 @@ a:hover,
a:visited, a:visited,
a:link, a:link,
.link { .link {
border-bottom: 1px solid rgb(0 0 0 / 30%); border-bottom: 2px solid rgb(0 0 0 / 30%);
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
@ -624,6 +624,10 @@ figure {
margin-bottom: 0.6em; margin-bottom: 0.6em;
white-space: nowrap; white-space: nowrap;
.link {
border-bottom: none;
}
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
@ -645,9 +649,10 @@ figure {
} }
a, a,
.link,
.linkReplacement, .linkReplacement,
button { button {
border-bottom: 2px solid transparent; border-bottom: 1px solid transparent;
color: var(--link-color); color: var(--link-color);
cursor: pointer; cursor: pointer;
font-weight: inherit; font-weight: inherit;
@ -662,6 +667,7 @@ figure {
font-weight: bold; font-weight: bold;
a, a,
.link,
.linkReplacement, .linkReplacement,
button { button {
border-bottom: 2px solid #000; border-bottom: 2px solid #000;