parent
3a61460af1
commit
6ae2fb07fc
|
@ -393,5 +393,12 @@
|
|||
"user already exist": "user already exists",
|
||||
"video": "video",
|
||||
"view": "view",
|
||||
"zine": "zine"
|
||||
"zine": "zine",
|
||||
"subscriber": "subscriber",
|
||||
"subscriber_rp": "subscriber",
|
||||
"subscribers": "subscribers",
|
||||
"subscription": "subscription",
|
||||
"subscription_rp": "subscription",
|
||||
"subscriptions": "subscriptions",
|
||||
"Users": "Users"
|
||||
}
|
||||
|
|
|
@ -300,6 +300,7 @@
|
|||
"Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||
"Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||
"Subscription": "Подписка",
|
||||
"subscription": "подписка",
|
||||
"Subscriptions": "Подписки",
|
||||
"Substrate": "Подложка",
|
||||
"Success": "Успешно",
|
||||
|
@ -387,7 +388,6 @@
|
|||
"email not confirmed": "email не подтвержден",
|
||||
"enter": "войдите",
|
||||
"feed": "лента",
|
||||
"follower": "подписчик",
|
||||
"general feed": "Общая лента",
|
||||
"header 1": "заголовок 1",
|
||||
"header 2": "заголовок 2",
|
||||
|
@ -419,5 +419,12 @@
|
|||
"video": "видео",
|
||||
"view": "просмотр",
|
||||
"zine": "журнал",
|
||||
"Enter footnote text": "Введите текст сноски"
|
||||
"Enter footnote text": "Введите текст сноски",
|
||||
"follower": "подписчик",
|
||||
"subscriber": "подписчик",
|
||||
"subscriber_rp": "подписчика",
|
||||
"subscribers": "подписчиков",
|
||||
"subscription_rp": "подписки",
|
||||
"subscriptions": "подписок",
|
||||
"Users": "Пользователи"
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ export const AudioPlayer = (props: Props) => {
|
|||
() => currentTrackIndex(),
|
||||
() => {
|
||||
setCurrentTrackDuration(0)
|
||||
}
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -441,3 +441,25 @@
|
|||
width: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribers {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
align-items: center;
|
||||
|
||||
.userpic {
|
||||
background: var(--background-color);
|
||||
box-shadow: 0 0 0 2px var(--background-color);
|
||||
vertical-align: top;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -2.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
}
|
|
@ -1,20 +1,25 @@
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import { Userpic } from './Userpic'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import type { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import styles from './AuthorCard.module.scss'
|
||||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import { translit } from '../../utils/ru2en'
|
||||
import { follow, unfollow } from '../../stores/zine/common'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { StatMetrics } from '../_shared/StatMetrics'
|
||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import { router, useRouter } from '../../stores/router'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { openPage } from '@nanostores/router'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { showModal } from '../../../stores/ui'
|
||||
import { TopicCard } from '../../Topic/Card'
|
||||
import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension'
|
||||
|
||||
interface AuthorCardProps {
|
||||
type SubscriptionFilter = 'all' | 'users' | 'topics'
|
||||
type AuthorCardProps = {
|
||||
caption?: string
|
||||
hideWriteButton?: boolean
|
||||
hideDescription?: boolean
|
||||
|
@ -32,6 +37,12 @@ interface AuthorCardProps {
|
|||
isFeedMode?: boolean
|
||||
isNowrap?: boolean
|
||||
class?: string
|
||||
followers?: Author[]
|
||||
subscriptions?: Array<Author | Topic>
|
||||
}
|
||||
|
||||
function isAuthor(value: Author | Topic): value is Author {
|
||||
return 'name' in value
|
||||
}
|
||||
|
||||
export const AuthorCard = (props: AuthorCardProps) => {
|
||||
|
@ -44,6 +55,8 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
} = useSession()
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>(props.subscriptions)
|
||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
|
||||
const subscribed = createMemo<boolean>(() => {
|
||||
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
|
@ -89,6 +102,18 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
}, 'subscribe')
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.subscriptions) {
|
||||
if (subscriptionFilter() === 'users') {
|
||||
setSubscriptions(props.subscriptions.filter((s) => 'name' in s))
|
||||
} else if (subscriptionFilter() === 'topics') {
|
||||
setSubscriptions(props.subscriptions.filter((s) => 'title' in s))
|
||||
} else {
|
||||
setSubscriptions(props.subscriptions)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.author, props.class)}
|
||||
|
@ -136,19 +161,20 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
}}
|
||||
>
|
||||
<div class={styles.authorDetailsWrapper}>
|
||||
<Show when={props.hasLink}>
|
||||
<div class={styles.authorNameContainer}>
|
||||
<ConditionalWrapper
|
||||
condition={props.hasLink}
|
||||
wrapper={(children) => (
|
||||
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
||||
{name()}
|
||||
{children}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!props.hasLink}>
|
||||
<div class={styles.authorName}>{name()}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.hideDescription && props.author.bio}>
|
||||
{props.isAuthorsList}
|
||||
<Show when={props.author.bio}>
|
||||
<div
|
||||
class={styles.authorAbout}
|
||||
classList={{ 'text-truncate': props.truncateBio }}
|
||||
|
@ -156,8 +182,44 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.author.stat}>
|
||||
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={props.author.stat} />
|
||||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<div class={styles.subscribers} onClick={() => showModal('followers')}>
|
||||
<For each={props.followers.slice(0, 3)}>
|
||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||
</For>
|
||||
<div>
|
||||
{props.followers.length}
|
||||
{getNumeralsDeclension(props.followers.length, [
|
||||
t('subscriber'),
|
||||
t('subscriber_rp'),
|
||||
t('subscribers')
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.subscriptions && props.subscriptions.length > 0}>
|
||||
<div>
|
||||
<div class={styles.subscribers} onClick={() => showModal('subscriptions')}>
|
||||
<For each={props.subscriptions.slice(0, 3)}>
|
||||
{(f) => {
|
||||
if ('name' in f) {
|
||||
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
||||
} else if ('title' in f) {
|
||||
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
||||
}
|
||||
return null
|
||||
}}
|
||||
</For>
|
||||
<div>
|
||||
{props.subscriptions.length}
|
||||
{getNumeralsDeclension(props.subscriptions.length, [
|
||||
t('subscription'),
|
||||
t('subscription_rp'),
|
||||
t('subscriptions')
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<ShowOnlyOnClient>
|
||||
|
@ -256,6 +318,68 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
</Show>
|
||||
</ShowOnlyOnClient>
|
||||
</div>
|
||||
<Show when={props.followers}>
|
||||
<Modal variant="wide" name="followers">
|
||||
<>
|
||||
<h2>{t('Followers')}</h2>
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<For each={props.followers}>
|
||||
{(follower: Author) => (
|
||||
<div class="col-xs-12">
|
||||
<AuthorCard author={follower} hideWriteButton={true} hasLink={true} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
</Show>
|
||||
<Show when={props.subscriptions}>
|
||||
<Modal variant="wide" name="subscriptions">
|
||||
<>
|
||||
<h2>{t('Subscriptions')}</h2>
|
||||
<ul class="view-switcher">
|
||||
<li class={clsx({ 'view-switcher__item--selected': true })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||
{t('All')} {props.subscriptions.length}
|
||||
</button>
|
||||
</li>
|
||||
<li class={clsx({ 'view-switcher__item--selected': false })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
||||
{t('Users')} {props.subscriptions.filter((s) => 'name' in s).length}
|
||||
</button>
|
||||
</li>
|
||||
<li class={clsx({ 'view-switcher__item--selected': false })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||
{t('Topics')} {props.subscriptions.filter((s) => 'title' in s).length}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class={styles.listWrapper}>
|
||||
<div class="row">
|
||||
<For each={subscriptions()}>
|
||||
{(subscription: Author | Topic) => (
|
||||
<div class="col-xs-12">
|
||||
{isAuthor(subscription) ? (
|
||||
<AuthorCard
|
||||
author={subscription}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
) : (
|
||||
<TopicCard compact isTopicInRow showDescription topic={subscription} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Author/AuthorCard/index.ts
Normal file
1
src/components/Author/AuthorCard/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AuthorCard } from './AuthorCard'
|
|
@ -1,9 +0,0 @@
|
|||
.userDetails {
|
||||
border-bottom: 2px solid #000;
|
||||
margin: 0 0 3.6rem;
|
||||
padding-bottom: 3.6rem;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import type { Author } from '../../graphql/types.gen'
|
||||
import { AuthorCard } from './AuthorCard'
|
||||
import styles from './Full.module.scss'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export const AuthorFull = (props: { author: Author }) => {
|
||||
return (
|
||||
<div class={clsx(styles.userDetails)}>
|
||||
<AuthorCard author={props.author} isAuthorPage={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -34,56 +34,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.userpic {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
display: inline-block;
|
||||
margin-right: -1.2rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.subscribers {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: -0.4rem 2em 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.subscribersCounter {
|
||||
@include font-size(1rem);
|
||||
background: #fff;
|
||||
border: 2px solid #000;
|
||||
border-radius: 100%;
|
||||
font-weight: bold;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 32px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.subscribersList {
|
||||
max-height: 15em;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.subscriber {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
padding: 8px 4px;
|
||||
transition: background 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadingWrapper {
|
||||
position: relative;
|
||||
min-height: 40vh;
|
||||
|
|
|
@ -3,7 +3,6 @@ import type { Author, Shout, Topic } from '../../../graphql/types.gen'
|
|||
import { Row1 } from '../../Feed/Row1'
|
||||
import { Row2 } from '../../Feed/Row2'
|
||||
import { Row3 } from '../../Feed/Row3'
|
||||
import { AuthorFull } from '../../Author/Full'
|
||||
|
||||
import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||
|
@ -13,8 +12,6 @@ import { splitToPages } from '../../../utils/splitToPages'
|
|||
import styles from './Author.module.scss'
|
||||
import stylesArticle from '../../Article/Article.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { Popup } from '../../_shared/Popup'
|
||||
import { AuthorCard } from '../../Author/AuthorCard'
|
||||
import { apiClient } from '../../../utils/apiClient'
|
||||
import { Comment } from '../../Article/Comment'
|
||||
|
@ -22,6 +19,7 @@ import { useLocalize } from '../../../context/localize'
|
|||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||
import { TopicCard } from '../../Topic/Card'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
|
||||
type AuthorProps = {
|
||||
shouts: Shout[]
|
||||
|
@ -30,24 +28,12 @@ type AuthorProps = {
|
|||
}
|
||||
|
||||
export type AuthorPageSearchParams = {
|
||||
by:
|
||||
| ''
|
||||
| 'viewed'
|
||||
| 'rating'
|
||||
| 'commented'
|
||||
| 'recent'
|
||||
| 'subscriptions'
|
||||
| 'followers'
|
||||
| 'about'
|
||||
| 'popular'
|
||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'about' | 'popular'
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||
const LOAD_MORE_PAGE_SIZE = 9
|
||||
|
||||
function isAuthor(value: Author | Topic): value is Author {
|
||||
return 'name' in value
|
||||
}
|
||||
export const AuthorView = (props: AuthorProps) => {
|
||||
const { t } = useLocalize()
|
||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||
|
@ -59,7 +45,6 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>([])
|
||||
const [isLoaded, setIsLoaded] = createSignal<boolean>()
|
||||
|
||||
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
|
||||
try {
|
||||
|
@ -77,6 +62,7 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
hideModal()
|
||||
try {
|
||||
const userSubscribers = await apiClient.getAuthorFollowers({ slug: props.authorSlug })
|
||||
setFollowers(userSubscribers)
|
||||
|
@ -89,6 +75,8 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||
await loadMore()
|
||||
}
|
||||
const { authors, topics } = await fetchSubscriptions()
|
||||
setSubscriptions([...authors, ...topics])
|
||||
})
|
||||
|
||||
const loadMore = async () => {
|
||||
|
@ -118,13 +106,6 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
const [commented, setCommented] = createSignal([])
|
||||
|
||||
createEffect(async () => {
|
||||
if (searchParams().by === 'subscriptions') {
|
||||
setIsLoaded(false)
|
||||
const { authors, topics } = await fetchSubscriptions()
|
||||
setSubscriptions([...authors, ...topics])
|
||||
setIsLoaded(true)
|
||||
}
|
||||
|
||||
if (searchParams().by === 'commented') {
|
||||
try {
|
||||
const data = await apiClient.getReactionsBy({
|
||||
|
@ -140,7 +121,12 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
<div class={styles.authorPage}>
|
||||
<div class="wide-container">
|
||||
<Show when={author()}>
|
||||
<AuthorFull author={author()} />
|
||||
<AuthorCard
|
||||
author={author()}
|
||||
isAuthorPage={true}
|
||||
followers={followers()}
|
||||
subscriptions={subscriptions()}
|
||||
/>
|
||||
</Show>
|
||||
<div class={clsx(styles.groupControls, 'row')}>
|
||||
<div class="col-md-16">
|
||||
|
@ -150,16 +136,6 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
{t('Publications')}
|
||||
</button>
|
||||
</li>
|
||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'followers' }}>
|
||||
<button type="button" onClick={() => changeSearchParam('by', 'followers')}>
|
||||
{t('Followers')}
|
||||
</button>
|
||||
</li>
|
||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'subscriptions' }}>
|
||||
<button type="button" onClick={() => changeSearchParam('by', 'subscriptions')}>
|
||||
{t('Subscriptions')}
|
||||
</button>
|
||||
</li>
|
||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'commented' }}>
|
||||
<button type="button" onClick={() => changeSearchParam('by', 'commented')}>
|
||||
{t('Comments')}
|
||||
|
@ -173,45 +149,6 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
</ul>
|
||||
</div>
|
||||
<div class={clsx('col-md-8', styles.additionalControls)}>
|
||||
<Popup
|
||||
trigger={
|
||||
<div class={styles.subscribers}>
|
||||
<Switch>
|
||||
<Match when={followers().length <= 3}>
|
||||
<For each={followers().slice(0, 3)}>
|
||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={followers().length > 3}>
|
||||
<For each={followers().slice(0, 2)}>
|
||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||
</For>
|
||||
<div class={clsx(styles.userpic, styles.subscribersCounter)}>
|
||||
{followers().length}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<ul class={clsx('nodash', styles.subscribersList)}>
|
||||
<For each={followers()}>
|
||||
{(item: Author) => (
|
||||
<li class={styles.subscriber}>
|
||||
<AuthorCard
|
||||
author={item}
|
||||
isNowrap={true}
|
||||
hideDescription={true}
|
||||
hideFollow={true}
|
||||
hasLink={true}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Popup>
|
||||
|
||||
<div class={styles.ratingContainer}>
|
||||
{t('Karma')}
|
||||
<AuthorRatingControl author={props.author} class={styles.ratingControl} />
|
||||
|
@ -239,52 +176,7 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={searchParams().by === 'followers'}>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<For each={followers()}>
|
||||
{(follower: Author) => (
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<AuthorCard author={follower} hideWriteButton={true} hasLink={true} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={searchParams().by === 'subscriptions'}>
|
||||
<div class={clsx('wide-container', styles.subscriptions)}>
|
||||
<div class="row position-relative">
|
||||
<Show
|
||||
when={isLoaded()}
|
||||
fallback={
|
||||
<div class={styles.loadingWrapper}>
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={subscriptions()}>
|
||||
{(subscription: Author | Topic) => (
|
||||
<div class="col-md-20 col-lg-18">
|
||||
{isAuthor(subscription) ? (
|
||||
<div class={styles.authorContainer}>
|
||||
<AuthorCard
|
||||
author={subscription}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<TopicCard compact isTopicInRow showDescription topic={subscription} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={searchParams().by === 'rating'}>
|
||||
<Show when={sortedArticles().length === 1}>
|
||||
<Row1 article={sortedArticles()[0]} noAuthorLink={true} />
|
||||
|
|
|
@ -55,13 +55,18 @@ export const FeedView = () => {
|
|||
actions: { loadReactionsBy }
|
||||
} = useReactions()
|
||||
|
||||
onMount(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => page().route + searchParams().by,
|
||||
() => {
|
||||
resetSortedArticles()
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ export const SolidSwiper = (props: Props) => {
|
|||
() => {
|
||||
mainSwipeRef.current?.swiper.update()
|
||||
thumbSwipeRef.current?.swiper.update()
|
||||
}
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -95,7 +96,7 @@ export const SolidSwiper = (props: Props) => {
|
|||
const results: UploadedFile[] = []
|
||||
for (const file of selectedFiles) {
|
||||
const result = await handleFileUpload(file)
|
||||
results.push(result.url)
|
||||
results.push(result)
|
||||
}
|
||||
props.onImagesAdd(composeMediaItems(results))
|
||||
setLoading(false)
|
||||
|
|
|
@ -17,14 +17,15 @@ export const AuthorPage = (props: PageProps) => {
|
|||
Boolean(props.authorShouts) && Boolean(props.author) && props.author.slug === slug()
|
||||
)
|
||||
|
||||
const preload = () =>
|
||||
Promise.all([
|
||||
const preload = () => {
|
||||
return Promise.all([
|
||||
loadShouts({
|
||||
filters: { author: slug(), visibility: 'community' },
|
||||
limit: PRERENDERED_ARTICLES_COUNT
|
||||
}),
|
||||
loadAuthor({ slug: slug() })
|
||||
])
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (isLoaded()) {
|
||||
|
@ -44,7 +45,8 @@ export const AuthorPage = (props: PageProps) => {
|
|||
resetSortedArticles()
|
||||
await preload()
|
||||
setIsLoaded(true)
|
||||
}
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -41,7 +41,8 @@ export const TopicPage = (props: PageProps) => {
|
|||
resetSortedArticles()
|
||||
await preload()
|
||||
setIsLoaded(true)
|
||||
}
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ export type ModalType =
|
|||
| 'uploadCoverImage'
|
||||
| 'editorInsertLink'
|
||||
| 'simplifiedEditorInsertLink'
|
||||
| 'followers'
|
||||
| 'subscriptions'
|
||||
|
||||
type WarnKind = 'error' | 'warn' | 'info'
|
||||
|
||||
|
@ -41,7 +43,9 @@ export const MODALS: Record<ModalType, ModalType> = {
|
|||
simplifiedEditorUploadImage: 'simplifiedEditorUploadImage',
|
||||
uploadCoverImage: 'uploadCoverImage',
|
||||
editorInsertLink: 'editorInsertLink',
|
||||
simplifiedEditorInsertLink: 'simplifiedEditorInsertLink'
|
||||
simplifiedEditorInsertLink: 'simplifiedEditorInsertLink',
|
||||
followers: 'followers',
|
||||
subscriptions: 'subscriptions'
|
||||
}
|
||||
|
||||
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { Author, Shout, ShoutInput, Topic, LoadShoutsOptions } from '../../graphql/types.gen'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import { addAuthorsByTopic } from './authors'
|
||||
import { addTopicsByAuthor } from './topics'
|
||||
import { byStat } from '../../utils/sortby'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createLazyMemo } from '@solid-primitives/memo'
|
||||
|
@ -97,26 +96,6 @@ const addArticles = (...args: Shout[][]) => {
|
|||
}, {} as { [topicSlug: string]: Author[] })
|
||||
|
||||
addAuthorsByTopic(authorsByTopic)
|
||||
|
||||
const topicsByAuthor = allArticles.reduce((acc, article) => {
|
||||
const { authors, topics } = article
|
||||
|
||||
authors.forEach((author) => {
|
||||
if (!acc[author.slug]) {
|
||||
acc[author.slug] = []
|
||||
}
|
||||
|
||||
topics.forEach((topic) => {
|
||||
if (!acc[author.slug].some((t) => t.slug === topic.slug)) {
|
||||
acc[author.slug].push(topic)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as { [authorSlug: string]: Topic[] })
|
||||
|
||||
addTopicsByAuthor(topicsByAuthor)
|
||||
}
|
||||
|
||||
const addSortedArticles = (articles: Shout[]) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { apiClient } from '../../utils/apiClient'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createEffect, createSignal } from 'solid-js'
|
||||
import { createLazyMemo } from '@solid-primitives/memo'
|
||||
import { byStat } from '../../utils/sortby'
|
||||
|
||||
|
@ -38,12 +38,18 @@ const addAuthors = (authors: Author[]) => {
|
|||
return acc
|
||||
}, {} as Record<string, Author>)
|
||||
|
||||
setAuthorEntities((prevAuthorEntities) => {
|
||||
return {
|
||||
...prevAuthorEntities,
|
||||
...newAuthorEntities
|
||||
setAuthorEntities((prevAuthorEntities) =>
|
||||
Object.keys(newAuthorEntities).reduce(
|
||||
(acc, authorSlug) => {
|
||||
acc[authorSlug] = {
|
||||
...acc[authorSlug],
|
||||
...newAuthorEntities[authorSlug]
|
||||
}
|
||||
})
|
||||
return acc
|
||||
},
|
||||
{ ...prevAuthorEntities }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
||||
|
|
|
@ -12,7 +12,6 @@ export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy)
|
|||
|
||||
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||
const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({})
|
||||
|
||||
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
||||
const topics = Object.values(topicEntities())
|
||||
|
@ -68,27 +67,6 @@ const addTopics = (...args: Topic[][]) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => {
|
||||
const allTopics = Object.values(newTopicsByAuthors).flat()
|
||||
addTopics(allTopics)
|
||||
|
||||
setTopicByAuthor((prevTopicsByAuthor) => {
|
||||
return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => {
|
||||
if (!acc[authorSlug]) {
|
||||
acc[authorSlug] = []
|
||||
}
|
||||
|
||||
topics.forEach((topic) => {
|
||||
if (!acc[authorSlug].some((t) => t.slug === topic.slug)) {
|
||||
acc[authorSlug].push(topic)
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, prevTopicsByAuthor)
|
||||
})
|
||||
}
|
||||
|
||||
export const loadAllTopics = async (): Promise<void> => {
|
||||
const topics = await apiClient.getAllTopics()
|
||||
addTopics(topics)
|
||||
|
@ -121,5 +99,5 @@ export const useTopicsStore = (initialState: InitialState = {}) => {
|
|||
setRandomTopics(initialState.randomTopics)
|
||||
}
|
||||
|
||||
return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor }
|
||||
return { topicEntities, sortedTopics, randomTopics, topTopics }
|
||||
}
|
||||
|
|
3
src/utils/getNumeralsDeclension.ts
Normal file
3
src/utils/getNumeralsDeclension.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Usage in tsx: {getNumeralsDeclension(NUMBER, ['яблоко', 'яблока', 'яблок'])}
|
||||
export const getNumeralsDeclension = (number: number, words: string[], cases = [2, 0, 1, 1, 1, 2]) =>
|
||||
words[number % 100 > 4 && number % 100 < 20 ? 2 : cases[number % 10 < 5 ? number % 10 : 5]]
|
|
@ -1,9 +1,10 @@
|
|||
import { UploadFile } from '@solid-primitives/upload'
|
||||
import { apiBaseUrl } from './config'
|
||||
import { UploadedFile } from '../pages/types'
|
||||
|
||||
const apiUrl = `${apiBaseUrl}/upload`
|
||||
|
||||
export const handleFileUpload = async (uploadFile: UploadFile) => {
|
||||
export const handleFileUpload = async (uploadFile: UploadFile): Promise<UploadedFile> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', uploadFile.file, uploadFile.name)
|
||||
const response = await fetch(apiUrl, {
|
||||
|
|
Loading…
Reference in New Issue
Block a user