Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/all-topics-page

This commit is contained in:
kvakazyambra 2024-04-25 23:38:04 +03:00
commit 7c9ecd1e3a
19 changed files with 254 additions and 252 deletions

View File

@ -528,5 +528,7 @@
"yesterday": "yesterday",
"Failed to delete comment": "Failed to delete comment",
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
"Restore password": "Restore password"
"Restore password": "Restore password",
"Subscribing...": "Subscribing...",
"Unsubscribing...": "Unsubscribing..."
}

View File

@ -555,5 +555,7 @@
"yesterday": "вчера",
"Failed to delete comment": "Не удалось удалить комментарий",
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
"Restore password": "Восстановить пароль"
"Restore password": "Восстановить пароль",
"Subscribing...": "Подписываем...",
"Unsubscribing...": "Отписываем..."
}

View File

@ -40,6 +40,7 @@ import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
//TODO: ProfileSubscriptionsPage - garbage code?
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page'

View File

@ -115,8 +115,4 @@
}
}
}
.actionButtonLabelHovered {
display: none;
}
}

View File

@ -10,14 +10,12 @@ import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router'
import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic'
import { FollowedInfo } from '../../../pages/types'
import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorBadge.module.scss'
type Props = {
@ -29,13 +27,19 @@ type Props = {
inviteView?: boolean
onInvite?: (id: number) => void
selected?: boolean
isFollowed?: FollowedInfo
}
export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery()
const { author, requireAuthentication } = useSession()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isFollowed, setIsFollowed] = createSignal<boolean>()
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
createEffect(() => {
if (!subscriptions || !props.author) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed)
})
createEffect(() => {
setIsMobileView(!mediaMatches.sm)
@ -67,20 +71,11 @@ export const AuthorBadge = (props: Props) => {
return props.author.name
})
createEffect(
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed?.value)
},
),
)
const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => {
setIsFollowed(value)
setFollowing(FollowingEntity.Author, props.author.slug, value)
isSubscribed()
? unfollow(FollowingEntity.Author, props.author.slug)
: follow(FollowingEntity.Author, props.author.slug)
}, 'subscribe')
}
@ -134,55 +129,13 @@ export const AuthorBadge = (props: Props) => {
</div>
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
<div class={styles.actions}>
<Show
when={!props.minimizeSubscribeButton}
fallback={<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />}
>
<Show
when={isFollowed()}
fallback={
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show when={props.iconButtons} fallback={t('Subscribe')}>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
<BadgeSubscribeButton
action={() => handleFollowClick()}
isSubscribed={isSubscribed()}
actionMessageType={
subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed(),
})}
/>
}
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed(),
})}
/>
</Show>
</Show>
<Show when={props.showMessageButton}>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}

View File

@ -34,16 +34,18 @@ export const AuthorCard = (props: Props) => {
const { author, isSessionLoaded, requireAuthentication } = useSession()
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>()
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { setFollowing, isOwnerSubscribed } = useFollowing()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
onMount(() => {
setAuthorSubs(props.following)
})
createEffect(() => {
setIsFollowed(isOwnerSubscribed(props.author?.id))
if (!subscriptions || !props.author) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed)
})
const name = createMemo(() => {
@ -83,15 +85,19 @@ export const AuthorCard = (props: Props) => {
})
const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => {
setIsFollowed(value)
setFollowing(FollowingEntity.Author, props.author.slug, value)
isSubscribed()
? unfollow(FollowingEntity.Author, props.author.slug)
: follow(FollowingEntity.Author, props.author.slug)
}, 'subscribe')
}
const followButtonText = createMemo(() => {
if (isOwnerSubscribed(props.author?.id)) {
if (subscribeInAction()?.slug === props.author.slug) {
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
}
if (isSubscribed()) {
return (
<>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
@ -119,12 +125,7 @@ export const AuthorCard = (props: Props) => {
<Show when={props.author.bio}>
<div class={styles.authorAbout} innerHTML={props.author.bio} />
</Show>
<Show
when={
(props.followers && props.followers.length > 0) ||
(props.following && props.following.length > 0)
}
>
<Show when={props.followers?.length > 0 || props.following?.length > 0}>
<div class={styles.subscribersContainer}>
<Show when={props.followers && props.followers.length > 0}>
<a href="?m=followers" class={styles.subscribers}>
@ -204,13 +205,14 @@ export const AuthorCard = (props: Props) => {
when={isProfileOwner()}
fallback={
<div class={styles.authorActions}>
<Show when={authorSubs().length}>
<Show when={authorSubs()?.length}>
<Button
onClick={handleFollowClick}
disabled={Boolean(subscribeInAction())}
value={followButtonText()}
isSubscribeButton={true}
class={clsx({
[stylesButton.subscribed]: isFollowed(),
[stylesButton.subscribed]: isSubscribed(),
})}
/>
</Show>
@ -255,15 +257,7 @@ export const AuthorCard = (props: Props) => {
<div class="row">
<div class="col-24">
<For each={props.followers}>
{(follower: Author) => (
<AuthorBadge
author={follower}
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(follower.id),
}}
/>
)}
{(follower: Author) => <AuthorBadge author={follower} />}
</For>
</div>
</div>
@ -318,21 +312,9 @@ export const AuthorCard = (props: Props) => {
<For each={authorSubs()}>
{(subscription) =>
isAuthor(subscription) ? (
<AuthorBadge
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id),
}}
author={subscription}
/>
<AuthorBadge author={subscription} />
) : (
<TopicBadge
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id),
}}
topic={subscription}
/>
<TopicBadge topic={subscription} />
)
}
</For>

View File

@ -21,7 +21,6 @@ const PAGE_SIZE = 20
export const AuthorsList = (props: Props) => {
const { t } = useLocalize()
const { isOwnerSubscribed } = useFollowing()
const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
const [loading, setLoading] = createSignal(false)
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
@ -83,13 +82,7 @@ export const AuthorsList = (props: Props) => {
{(author) => (
<div class="row">
<div class="col-lg-20 col-xl-18">
<AuthorBadge
author={author}
isFollowed={{
loaded: !loading(),
value: isOwnerSubscribed(author.id),
}}
/>
<AuthorBadge author={author} />
</div>
</div>
)}

View File

@ -30,7 +30,6 @@ type Props = {
export const Beside = (props: Props) => {
const { t } = useLocalize()
const { isOwnerSubscribed } = useFollowing()
return (
<Show when={!!props.beside?.slug && props.values?.length > 0}>
@ -86,12 +85,7 @@ export const Beside = (props: Props) => {
/>
</Show>
<Show when={props.wrapper === 'author'}>
<AuthorBadge
author={value as Author}
isFollowed={{
value: isOwnerSubscribed(value.id),
}}
/>
<AuthorBadge author={value as Author} />
</Show>
<Show when={props.wrapper === 'article' && value?.slug}>
<ArticleCard

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx'
import { Show, createMemo, createSignal } from 'solid-js'
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize'
@ -38,14 +38,18 @@ export const TopicCard = (props: TopicProps) => {
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
)
const { author, requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal()
const [isSubscribed, setIsSubscribed] = createSignal()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => {
if (!subscriptions || !props.topic) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed)
})
const handleFollowClick = () => {
const value = !followed()
requireAuthentication(() => {
setFollowed(value)
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
follow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe')
}
@ -53,12 +57,12 @@ export const TopicCard = (props: TopicProps) => {
return (
<>
<Show when={props.iconButton}>
<Show when={followed()} fallback="+">
<Show when={isSubscribed()} fallback="+">
<Icon name="check-subscribed" />
</Show>
</Show>
<Show when={!props.iconButton}>
<Show when={followed()} fallback={t('Follow')}>
<Show when={isSubscribed()} fallback={t('Follow')}>
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
</Show>
@ -130,7 +134,7 @@ export const TopicCard = (props: TopicProps) => {
fallback={
<CheckButton
text={t('Follow')}
checked={Boolean(followed())}
checked={Boolean(isSubscribed())}
onClick={handleFollowClick}
/>
}
@ -142,10 +146,10 @@ export const TopicCard = (props: TopicProps) => {
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.isSubscribing]: subLoading(),
[stylesButton.subscribed]: followed(),
[styles.isSubscribing]:
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined,
[stylesButton.subscribed]: isSubscribed(),
})}
// disabled={subLoading()}
/>
</Show>
</Show>

View File

@ -8,16 +8,12 @@ import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { capitalize } from '../../../utils/capitalize'
import { getImageUrl } from '../../../utils/getImageUrl'
import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton'
import { FollowedInfo } from '../../../pages/types'
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
import styles from './TopicBadge.module.scss'
type Props = {
topic: Topic
minimizeSubscribeButton?: boolean
isFollowed?: FollowedInfo
showStat?: boolean
}
@ -26,14 +22,20 @@ export const TopicBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const { requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [isFollowed, setIsFollowed] = createSignal<boolean>()
const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => {
if (!subscriptions || !props.topic) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed)
})
const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => {
setIsFollowed(value)
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
isSubscribed()
? follow(FollowingEntity.Topic, props.topic.slug)
: unfollow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe')
}
@ -41,15 +43,6 @@ export const TopicBadge = (props: Props) => {
setIsMobileView(!mediaMatches.sm)
})
createEffect(
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed.value)
},
),
)
const title = () =>
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
@ -71,35 +64,14 @@ export const TopicBadge = (props: Props) => {
</Show>
</a>
</div>
<div class={styles.actions}>
<Show
when={!props.minimizeSubscribeButton}
fallback={
<CheckButton text={t('Follow')} checked={Boolean(isFollowed())} onClick={handleFollowClick} />
<BadgeSubscribeButton
isSubscribed={isSubscribed()}
action={handleFollowClick}
actionMessageType={
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
}
>
<Show
when={isFollowed()}
fallback={
<Button
variant="primary"
size="S"
value={subLoading() ? t('subscribing...') : t('Subscribe')}
onClick={handleFollowClick}
class={styles.subscribeButton}
/>
}
>
<Button
onClick={handleFollowClick}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton}
/>
</Show>
</Show>
</div>
</div>
<div class={styles.stats}>

View File

@ -74,8 +74,6 @@ export const AllTopics = (props: Props) => {
return keys
})
const { isOwnerSubscribed } = useFollowing()
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchQuery, setSearchQuery] = createSignal('')
const filteredResults = createMemo(() => {
@ -188,14 +186,7 @@ export const AllTopics = (props: Props) => {
<For each={filteredResults().slice(0, limit())}>
{(topic) => (
<>
<TopicBadge
topic={topic}
isFollowed={{
loaded: filteredResults().length > 0,
value: isOwnerSubscribed(topic.slug),
}}
showStat={true}
/>
<TopicBadge topic={topic} showStat={true} />
</>
)}
</For>

View File

@ -3,7 +3,7 @@ import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/cor
import { getPagePath } from '@nanostores/router'
import { Meta, Title } from '@solidjs/meta'
import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from 'solid-js'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
@ -75,7 +75,7 @@ export const AuthorView = (props: Props) => {
const bioContainerRef: { current: HTMLDivElement } = { current: null }
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
const fetchData = async (slug) => {
const fetchData = async (slug: string) => {
try {
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
apiClient.getAuthorFollows({ slug }),
@ -118,7 +118,6 @@ export const AuthorView = (props: Props) => {
// pagination
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore()
loadSubscriptions()
}
})
@ -135,6 +134,7 @@ export const AuthorView = (props: Props) => {
createEffect(() => {
if (author()) {
fetchData(author().slug)
fetchComments(author())
}
})

View File

@ -1,16 +1,14 @@
import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { apiClient } from '../../../graphql/client/core'
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen'
import { LayoutType } from '../../../pages/types'
import { router } from '../../../stores/router'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
import { getUnixtime } from '../../../utils/getServerDate'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { splitToPages } from '../../../utils/splitToPages'
import { ArticleCard } from '../../Feed/ArticleCard'
import { Button } from '../../_shared/Button'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
@ -28,19 +26,12 @@ export const PRERENDERED_ARTICLES_COUNT = 36
const LOAD_MORE_PAGE_SIZE = 12
export const Expo = (props: Props) => {
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
const { t } = useLocalize()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
const { t } = useLocalize()
const { sortedArticles } = useArticlesStore({
shouts: isLoaded() ? props.shouts : [],
layout: props.layout,
})
const [articlesEndPage, setArticlesEndPage] = createSignal<number>(PRERENDERED_ARTICLES_COUNT)
const [expoShouts, setExpoShouts] = createSignal<Shout[]>([])
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => {
const filters = { ...additionalFilters }
@ -58,15 +49,18 @@ export const Expo = (props: Props) => {
const options: LoadShoutsOptions = {
filters: getLoadShoutsFilters(),
limit: count,
offset: sortedArticles().length,
offset: expoShouts().length,
}
options.filters = props.layout
? { layouts: [props.layout] }
: { layouts: ['audio', 'video', 'image', 'literature'] }
const { hasMore } = await loadShouts(options)
const newShouts = await apiClient.getShouts(options)
const hasMore = newShouts?.length !== options.limit + 1 && newShouts?.length !== 0
setIsLoadMoreButtonVisible(hasMore)
setExpoShouts((prev) => [...prev, ...newShouts])
}
const loadMoreWithoutScrolling = async (count: number) => {
@ -100,19 +94,7 @@ export const Expo = (props: Props) => {
}
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)
}
loadRandomTopArticles()
loadRandomTopMonthArticles()
})
@ -121,9 +103,11 @@ export const Expo = (props: Props) => {
on(
() => props.layout,
() => {
resetSortedArticles()
setExpoShouts([])
setIsLoadMoreButtonVisible(false)
setFavoriteTopArticles([])
setReactedTopMonthArticles([])
setArticlesEndPage(PRERENDERED_ARTICLES_COUNT)
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
loadRandomTopArticles()
loadRandomTopMonthArticles()
@ -132,16 +116,17 @@ export const Expo = (props: Props) => {
)
onCleanup(() => {
resetSortedArticles()
setExpoShouts([])
})
const handleLoadMoreClick = () => {
loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE)
setArticlesEndPage((prev) => prev + LOAD_MORE_PAGE_SIZE)
}
return (
<div class={styles.Expo}>
<Show when={sortedArticles()?.length > 0} fallback={<Loading />}>
<Show when={expoShouts().length > 0} fallback={<Loading />}>
<div class="wide-container">
<ul class={clsx('view-switcher')}>
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
@ -194,7 +179,7 @@ export const Expo = (props: Props) => {
</li>
</ul>
<div class="row">
<For each={sortedArticles().slice(0, LOAD_MORE_PAGE_SIZE)}>
<For each={expoShouts()?.slice(0, LOAD_MORE_PAGE_SIZE)}>
{(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard
@ -209,7 +194,7 @@ export const Expo = (props: Props) => {
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Top month articles')} slides={reactedTopMonthArticles()} />
</Show>
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
<For each={expoShouts().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
@ -224,7 +209,7 @@ export const Expo = (props: Props) => {
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
</Show>
<For each={sortedArticles().slice(LOAD_MORE_PAGE_SIZE * 2)}>
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE * 2, articlesEndPage())}>
{(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard

View File

@ -0,0 +1,29 @@
.actionButton {
border-radius: 0.8rem !important;
margin-right: 0 !important;
width: 9em;
&.iconed {
padding: 6px !important;
min-width: 4rem;
width: unset;
&:hover img {
filter: invert(1);
}
}
&:hover {
.actionButtonLabel {
display: none;
}
.actionButtonLabelHovered {
display: block;
}
}
}
.actionButtonLabelHovered {
display: none;
}

View File

@ -0,0 +1,87 @@
import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { Button } from '../Button'
import stylesButton from '../Button/Button.module.scss'
import { CheckButton } from '../CheckButton'
import { Icon } from '../Icon'
import styles from './BadgeDubscribeButton.module.scss'
type Props = {
class?: string
isSubscribed: boolean
minimizeSubscribeButton?: boolean
action: () => void
iconButtons?: boolean
actionMessageType?: 'subscribe' | 'unsubscribe'
}
export const BadgeSubscribeButton = (props: Props) => {
const { t } = useLocalize()
const inActionText = createMemo(() => {
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
})
return (
<div class={props.class}>
<Show
when={!props.minimizeSubscribeButton}
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />}
>
<Show
when={props.isSubscribed}
fallback={
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={props.actionMessageType ? inActionText() : t('Subscribe')}
>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
}
onClick={props.action}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed,
})}
/>
}
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={
props.actionMessageType ? (
inActionText()
) : (
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
)
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={props.action}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed,
})}
/>
</Show>
</Show>
</div>
)
}

View File

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

View File

@ -1,11 +1,19 @@
import { Accessor, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js'
import { Accessor, JSX, createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
import { createStore } from 'solid-js/store'
import { apiClient } from '../graphql/client/core'
import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
import { Author, AuthorFollowsResult, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
import { useSession } from './session'
export type SubscriptionsData = {
topics?: Topic[]
authors?: Author[]
communities?: Community[]
}
type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
interface FollowingContextType {
loading: Accessor<boolean>
followers: Accessor<Array<Author>>
@ -15,7 +23,8 @@ interface FollowingContextType {
loadSubscriptions: () => void
follow: (what: FollowingEntity, slug: string) => Promise<void>
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
isOwnerSubscribed: (id: number | string) => boolean
// followers: Accessor<Author[]>
subscribeInAction?: Accessor<SubscribeAction>
}
const FollowingContext = createContext<FollowingContextType>()
@ -43,7 +52,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
console.debug('[context.following] fetching subs data...')
const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
console.info('[context.following] subs:', subscriptions)
}
} catch (error) {
console.info('[context.following] cannot get subs', error)
@ -52,28 +60,37 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
}
}
createEffect(() => {
console.info('[context.following] subs:', subscriptions)
})
const [subscribeInAction, setSubscribeInAction] = createSignal<SubscribeAction>()
const follow = async (what: FollowingEntity, slug: string) => {
if (!author()) return
setSubscribeInAction({ slug, type: 'subscribe' })
try {
await apiClient.follow({ what, slug })
const subscriptionData = await apiClient.follow({ what, slug })
setSubscriptions((prevSubscriptions) => {
const updatedSubs = { ...prevSubscriptions }
if (!updatedSubs[what]) updatedSubs[what] = []
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
if (!exists) updatedSubs[what].push(slug)
return updatedSubs
if (!prevSubscriptions[what]) prevSubscriptions[what] = []
prevSubscriptions[what].push(subscriptionData)
return prevSubscriptions
})
} catch (error) {
console.error(error)
} finally {
setSubscribeInAction() // Сбрасываем состояние действия подписки.
}
}
const unfollow = async (what: FollowingEntity, slug: string) => {
if (!author()) return
setSubscribeInAction({ slug: slug, type: 'unsubscribe' })
try {
await apiClient.unfollow({ what, slug })
} catch (error) {
console.error(error)
} finally {
setSubscribeInAction()
}
}
@ -114,23 +131,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
}
}
const isOwnerSubscribed = (id?: number | string) => {
if (!author() || !subscriptions) return
const isAuthorSubscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === id)
const isTopicSubscribed = subscriptions.topics?.some((topicEntity) => topicEntity.slug === id)
return !!isAuthorSubscribed || !!isTopicSubscribed
}
const value: FollowingContextType = {
loading,
subscriptions,
setSubscriptions,
isOwnerSubscribed,
setFollowing,
followers,
loadSubscriptions: fetchData,
follow,
unfollow,
// followers,
subscribeInAction,
}
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>

View File

@ -4,6 +4,10 @@ export default gql`
mutation FollowMutation($what: FollowingEntity!, $slug: String!) {
follow(what: $what, slug: $slug) {
error
authors {
id
slug
}
}
}
`

View File

@ -53,9 +53,4 @@ export type UploadedFile = {
originalFilename?: string
}
export type FollowedInfo = {
value?: boolean
loaded?: boolean
}
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'