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

View File

@ -1,14 +1,14 @@
import { capitalize, plural } from '../../utils'
import { capitalize } from '../../utils'
import styles from './Card.module.scss'
import { createMemo, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
import { locale } from '../../stores/ui'
import { follow, unfollow } from '../../stores/zine/common'
import { getLogger } from '../../utils/logger'
import { clsx } from 'clsx'
import { useSession } from '../../context/session'
import { StatMetrics } from '../_shared/StatMetrics'
const log = getLogger('TopicCard')
@ -74,54 +74,6 @@ export const TopicCard = (props: TopicProps) => {
{props.topic.body}
</div>
</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
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 { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl'
@ -11,9 +11,10 @@ import { locale } from '../../stores/ui'
import { translit } from '../../utils/ru2en'
import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics'
type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'rating'
by: '' | 'name' | 'shouts' | 'followers'
}
type Props = {
@ -24,23 +25,27 @@ const PAGE_SIZE = 20
const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ']
export const AllAuthorsView = (props: Props) => {
const { sortedAuthors } = useAuthorsStore({ authors: props.authors })
const [limit, setLimit] = createSignal(PAGE_SIZE)
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
const { sortedAuthors } = useAuthorsStore({
authors: props.authors,
sortBy: searchParams().by || 'shouts'
})
const { session } = useSession()
onMount(() => changeSearchParam('by', 'shouts'))
createEffect(() => {
setAuthorsSort(searchParams().by || 'shouts')
setLimit(PAGE_SIZE)
})
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce((acc, author) => {
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] = []
acc[letter].push(author)
return acc
@ -64,8 +69,8 @@ export const AllAuthorsView = (props: Props) => {
<li classList={{ selected: searchParams().by === 'shouts' }}>
<a href="/authors?by=shouts">{t('By shouts')}</a>
</li>
<li classList={{ selected: searchParams().by === 'rating' }}>
<a href="/authors?by=rating">{t('By rating')}</a>
<li classList={{ selected: searchParams().by === 'followers' }}>
<a href="/authors?by=followers">{t('By rating')}</a>
</li>
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
<a href="/authors?by=name">{t('By name')}</a>
@ -167,15 +172,18 @@ export const AllAuthorsView = (props: Props) => {
<Show when={searchResults().length > 0}>
<For each={searchResults().slice(0, limit())}>
{(author) => (
<AuthorCard
author={author}
compact={false}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
/>
<>
<AuthorCard
author={author}
compact={false}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)}
</For>
</Show>
@ -185,15 +193,18 @@ export const AllAuthorsView = (props: Props) => {
<div class="col-lg-10 col-xl-9">
<For each={sortedAuthors().slice(0, limit())}>
{(author) => (
<AuthorCard
author={author}
compact={false}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
/>
<>
<AuthorCard
author={author}
compact={false}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)}
</For>
</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 { t } from '../../utils/intl'
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
@ -11,6 +11,7 @@ import { translit } from '../../utils/ru2en'
import styles from '../../styles/AllTopics.module.scss'
import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics'
type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | ''
@ -34,6 +35,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const { session } = useSession()
onMount(() => changeSearchParam('by', 'shouts'))
createEffect(() => {
setTopicsSort(searchParams().by || 'shouts')
setLimit(PAGE_SIZE)
@ -182,12 +184,15 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
<Show when={searchParams().by && searchParams().by !== 'title'}>
<For each={sortedTopics().slice(0, limit())}>
{(topic) => (
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
<>
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
<StatMetrics fields={['shouts', 'authors', 'followers']} stat={topic.stat} />
</>
)}
</For>
</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 {
shouts
followers
comments: commented
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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