all-topics-all-authors-cosmetics-and-refactoring

This commit is contained in:
tonyrewin 2022-11-22 12:27:01 +03:00
parent 8dd394eee1
commit 3624004e03
12 changed files with 143 additions and 102 deletions

View File

@ -9,6 +9,7 @@ import { locale } from '../../stores/ui'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { StatMetrics } from '../_shared/StatMetrics'
interface AuthorCardProps { interface AuthorCardProps {
caption?: string caption?: string
@ -69,9 +70,11 @@ export const AuthorCard = (props: AuthorCardProps) => {
<Show when={!props.hideDescription}> <Show when={!props.hideDescription}>
{props.isAuthorsList} {props.isAuthorsList}
<div class={styles.authorAbout} classList={{ 'text-truncate': props.truncateBio }}> <div
{bio()} class={styles.authorAbout}
</div> classList={{ 'text-truncate': props.truncateBio }}
innerHTML={props.caption || bio()}
></div>
</Show> </Show>
</div> </div>

View File

@ -1,14 +1,14 @@
import { capitalize, plural } from '../../utils' import { capitalize } from '../../utils'
import styles from './Card.module.scss' import styles from './Card.module.scss'
import { createMemo, Show } from 'solid-js' import { createMemo, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { locale } from '../../stores/ui'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { getLogger } from '../../utils/logger' import { getLogger } from '../../utils/logger'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { StatMetrics } from '../_shared/StatMetrics'
const log = getLogger('TopicCard') const log = getLogger('TopicCard')
@ -74,54 +74,6 @@ export const TopicCard = (props: TopicProps) => {
{props.topic.body} {props.topic.body}
</div> </div>
</Show> </Show>
<Show when={props.topic?.stat}>
<div class={styles.topicDetails}>
<Show when={props.showPublications}>
<span class={styles.topicDetailsItem} classList={{ compact: props.compact }}>
{props.topic.stat?.shouts +
' ' +
t('post') +
plural(
props.topic.stat?.shouts || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span>
</Show>
<Show when={!props.compact}>
<span class={styles.topicDetailsItem} classList={{ compact: props.compact }}>
{props.topic.stat?.authors +
' ' +
t('author') +
plural(
props.topic.stat?.authors || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span>
<span class={styles.topicDetailsItem} classList={{ compact: props.compact }}>
{props.topic.stat?.followers +
' ' +
t('follower') +
plural(
props.topic.stat?.followers || 0,
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
)}
</span>
{/*FIXME*/}
{/*<Show when={false && !props.subscribeButtonBottom}>*/}
{/* <span class='topic-details__item'>*/}
{/* {topic().stat?.viewed +*/}
{/* ' ' +*/}
{/* t('view') +*/}
{/* plural(*/}
{/* topic().stat?.viewed || 0,*/}
{/* locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']*/}
{/* )}*/}
{/* </span>*/}
{/*</Show>*/}
</Show>
</div>
</Show>
</div> </div>
<div <div
class={styles.controlContainer} class={styles.controlContainer}

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
@ -11,9 +11,10 @@ import { locale } from '../../stores/ui'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics'
type AllAuthorsPageSearchParams = { type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'rating' by: '' | 'name' | 'shouts' | 'followers'
} }
type Props = { type Props = {
@ -24,23 +25,27 @@ const PAGE_SIZE = 20
const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'] const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ']
export const AllAuthorsView = (props: Props) => { export const AllAuthorsView = (props: Props) => {
const { sortedAuthors } = useAuthorsStore({ authors: props.authors })
const [limit, setLimit] = createSignal(PAGE_SIZE) const [limit, setLimit] = createSignal(PAGE_SIZE)
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
const { sortedAuthors } = useAuthorsStore({
authors: props.authors,
sortBy: searchParams().by || 'shouts'
})
const { session } = useSession() const { session } = useSession()
onMount(() => changeSearchParam('by', 'shouts'))
createEffect(() => { createEffect(() => {
setAuthorsSort(searchParams().by || 'shouts') setAuthorsSort(searchParams().by || 'shouts')
setLimit(PAGE_SIZE)
}) })
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || '')) const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce((acc, author) => { return sortedAuthors().reduce((acc, author) => {
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase() let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
if (!/[А-Я]/i.test(letter) && locale() === 'ru') letter = '@' if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '@'
if (!acc[letter]) acc[letter] = [] if (!acc[letter]) acc[letter] = []
acc[letter].push(author) acc[letter].push(author)
return acc return acc
@ -64,8 +69,8 @@ export const AllAuthorsView = (props: Props) => {
<li classList={{ selected: searchParams().by === 'shouts' }}> <li classList={{ selected: searchParams().by === 'shouts' }}>
<a href="/authors?by=shouts">{t('By shouts')}</a> <a href="/authors?by=shouts">{t('By shouts')}</a>
</li> </li>
<li classList={{ selected: searchParams().by === 'rating' }}> <li classList={{ selected: searchParams().by === 'followers' }}>
<a href="/authors?by=rating">{t('By rating')}</a> <a href="/authors?by=followers">{t('By rating')}</a>
</li> </li>
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}> <li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
<a href="/authors?by=name">{t('By name')}</a> <a href="/authors?by=name">{t('By name')}</a>
@ -167,15 +172,18 @@ export const AllAuthorsView = (props: Props) => {
<Show when={searchResults().length > 0}> <Show when={searchResults().length > 0}>
<For each={searchResults().slice(0, limit())}> <For each={searchResults().slice(0, limit())}>
{(author) => ( {(author) => (
<AuthorCard <>
author={author} <AuthorCard
compact={false} author={author}
hasLink={true} compact={false}
subscribed={subscribed(author.slug)} hasLink={true}
noSocialButtons={true} subscribed={subscribed(author.slug)}
isAuthorsList={true} noSocialButtons={true}
truncateBio={true} isAuthorsList={true}
/> truncateBio={true}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)} )}
</For> </For>
</Show> </Show>
@ -185,15 +193,18 @@ export const AllAuthorsView = (props: Props) => {
<div class="col-lg-10 col-xl-9"> <div class="col-lg-10 col-xl-9">
<For each={sortedAuthors().slice(0, limit())}> <For each={sortedAuthors().slice(0, limit())}>
{(author) => ( {(author) => (
<AuthorCard <>
author={author} <AuthorCard
compact={false} author={author}
hasLink={true} compact={false}
subscribed={subscribed(author.slug)} hasLink={true}
noSocialButtons={true} subscribed={subscribed(author.slug)}
isAuthorsList={true} noSocialButtons={true}
truncateBio={true} isAuthorsList={true}
/> truncateBio={true}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)} )}
</For> </For>
</div> </div>

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
@ -11,6 +11,7 @@ import { translit } from '../../utils/ru2en'
import styles from '../../styles/AllTopics.module.scss' import styles from '../../styles/AllTopics.module.scss'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics'
type AllTopicsPageSearchParams = { type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | '' by: 'shouts' | 'authors' | 'title' | ''
@ -34,6 +35,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const { session } = useSession() const { session } = useSession()
onMount(() => changeSearchParam('by', 'shouts'))
createEffect(() => { createEffect(() => {
setTopicsSort(searchParams().by || 'shouts') setTopicsSort(searchParams().by || 'shouts')
setLimit(PAGE_SIZE) setLimit(PAGE_SIZE)
@ -182,12 +184,15 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
<Show when={searchParams().by && searchParams().by !== 'title'}> <Show when={searchParams().by && searchParams().by !== 'title'}>
<For each={sortedTopics().slice(0, limit())}> <For each={sortedTopics().slice(0, limit())}>
{(topic) => ( {(topic) => (
<TopicCard <>
topic={topic} <TopicCard
compact={false} topic={topic}
subscribed={subscribed(topic.slug)} compact={false}
showPublications={true} subscribed={subscribed(topic.slug)}
/> showPublications={true}
/>
<StatMetrics fields={['shouts', 'authors', 'followers']} stat={topic.stat} />
</>
)} )}
</For> </For>
</Show> </Show>

View File

@ -0,0 +1,34 @@
.statMetrics {
@include font-size(1.7rem);
color: #9fa1a7;
display: flex;
margin-bottom: 1em;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
}
.statMetricsItem {
@include font-size(1.5rem);
margin-right: 1.6rem;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.compact {
font-size: small;
}
&.followers {
word-break: keep-all;
}
&.button {
float: right;
}
}

View File

@ -0,0 +1,35 @@
import { For } from 'solid-js'
import type { Stat, TopicStat } from '../../graphql/types.gen'
import { locale } from '../../stores/ui'
import { plural } from '../../utils'
import { t } from '../../utils/intl'
import styles from './Stat.module.scss'
interface StatMetricsProps {
fields?: string[]
stat: Stat | TopicStat
compact?: boolean
}
const pseudonames = {
comments: 'discussions'
}
const nos = (s) => s.slice(0, s.length - 1)
export const StatMetrics = (props: StatMetricsProps) => {
return (
<div class={styles.statMetrics}>
<For each={props.fields}>
{(entity: string) => (
<span class={styles.statMetricsItem} classList={{ compact: props.compact }}>
{props.stat[entity] +
' ' +
t(nos(pseudonames[entity] || entity)) +
plural(props.stat[entity] || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])}
</span>
)}
</For>
</div>
)
}

View File

@ -13,6 +13,7 @@ export default gql`
stat { stat {
shouts shouts
followers followers
comments: commented
} }
} }
} }

View File

@ -35,10 +35,10 @@ export type Author = {
} }
export type AuthorStat = { export type AuthorStat = {
rating?: Maybe<Scalars['Int']>
commented?: Maybe<Scalars['Int']> commented?: Maybe<Scalars['Int']>
followers?: Maybe<Scalars['Int']> followers?: Maybe<Scalars['Int']>
followings?: Maybe<Scalars['Int']> followings?: Maybe<Scalars['Int']>
rating?: Maybe<Scalars['Int']>
shouts?: Maybe<Scalars['Int']> shouts?: Maybe<Scalars['Int']>
} }
@ -615,6 +615,8 @@ export type Stat = {
rating?: Maybe<Scalars['Int']> rating?: Maybe<Scalars['Int']>
reacted?: Maybe<Scalars['Int']> reacted?: Maybe<Scalars['Int']>
viewed?: Maybe<Scalars['Int']> viewed?: Maybe<Scalars['Int']>
shouts?: Maybe<Scalars['Int']>
followers?: Maybe<Scalars['Int']>
} }
export type Subscription = { export type Subscription = {

View File

@ -178,5 +178,7 @@
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"view": "просмотр", "view": "просмотр",
"zine": "журнал" "zine": "журнал",
"shout": "пост",
"discussion": "дискурс"
} }

View File

@ -2,8 +2,9 @@ import { apiClient } from '../../utils/apiClient'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { createLazyMemo } from '@solid-primitives/memo' import { createLazyMemo } from '@solid-primitives/memo'
import { byStat } from '../../utils/sortby'
export type AuthorsSortBy = 'shouts' | 'name' | 'rating' export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts') const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
@ -15,21 +16,15 @@ const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]:
const sortedAuthors = createLazyMemo(() => { const sortedAuthors = createLazyMemo(() => {
const authors = Object.values(authorEntities()) const authors = Object.values(authorEntities())
switch (sortAllBy()) { switch (sortAllBy()) {
// case 'created': { case 'followers': {
// log.debug('sorted by created') authors.sort(byStat('followers'))
// authors.sort(byCreated)
// break
// }
case 'rating': {
// TODO:
break break
} }
case 'shouts': { case 'shouts': {
// TODO: authors.sort(byStat('shouts'))
break break
} }
case 'name': { case 'name': {
console.debug('sorted by name')
authors.sort((a, b) => a.name.localeCompare(b.name)) authors.sort((a, b) => a.name.localeCompare(b.name))
break break
} }
@ -84,9 +79,13 @@ export const loadAllAuthors = async (): Promise<void> => {
type InitialState = { type InitialState = {
authors?: Author[] authors?: Author[]
sortBy?: AuthorsSortBy
} }
export const useAuthorsStore = (initialState: InitialState = {}) => { export const useAuthorsStore = (initialState: InitialState = {}) => {
if (initialState.sortBy) {
setSortAllBy(initialState.sortBy)
}
addAuthors([...(initialState.authors || [])]) addAuthors([...(initialState.authors || [])])
return { authorEntities, sortedAuthors, authorsByTopic } return { authorEntities, sortedAuthors, authorsByTopic }

View File

@ -260,9 +260,7 @@ export const apiClient = {
}, },
getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => { getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => {
const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise()
resp.error ?? console.error(resp.error)
console.log('resactions response', resp)
return resp.data.loadReactionsBy return resp.data.loadReactionsBy
}, },

View File

@ -27,7 +27,6 @@ export const byLength = (
return 0 return 0
} }
// TODO more typing
export const byStat = (metric: keyof Stat) => { export const byStat = (metric: keyof Stat) => {
return (a, b) => { return (a, b) => {
const x = (a?.stat && a.stat[metric]) || 0 const x = (a?.stat && a.stat[metric]) || 0