diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index d4a43dd1..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npm run pre-commit diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 113182a4..5dfd02bb 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -291,6 +291,7 @@ "Profile": "Profile", "Publications": "Publications", "PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}", + "FollowersWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}}", "Publish Album": "Publish Album", "Publish Settings": "Publish Settings", "Published": "Published", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index bcdc7e2c..647917b0 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -309,9 +309,10 @@ "Publication settings": "Настройки публикации", "Publications": "Публикации", "PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}", + "FollowersWithCount": "{count, plural, =0 {нет подписчиков} one {{count} подписчик} few {{count} подписчика} other {{count} подписчиков}}", + "Publish": "Опубликовать", "Publish Album": "Опубликовать альбом", "Publish Settings": "Настройки публикации", - "Publish": "Опубликовать", "Published": "Опубликованные", "Punchline": "Панчлайн", "Quit": "Выйти", diff --git a/src/components/Author/AuthorBadge/AuthorBadge.module.scss b/src/components/Author/AuthorBadge/AuthorBadge.module.scss index e78f10ca..8dc68f4b 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.module.scss +++ b/src/components/Author/AuthorBadge/AuthorBadge.module.scss @@ -58,6 +58,11 @@ } .bio { + @include font-size(1.2rem); + + display: flex; + flex-direction: row; + gap: 1rem; color: var(--black-400); font-weight: 500; } diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 063cf8f7..e0ef0334 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -118,12 +118,17 @@ export const AuthorBadge = (props: Props) => {
- 0}> -
- {t('PublicationsWithCount', { count: props.author?.stat.shouts ?? 0 })} -
-
+ +
+ 0}> +
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
+
+ 0}> +
{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}
+
+
+
diff --git a/src/components/AuthorsList/AuthorsList.module.scss b/src/components/AuthorsList/AuthorsList.module.scss new file mode 100644 index 00000000..bad088be --- /dev/null +++ b/src/components/AuthorsList/AuthorsList.module.scss @@ -0,0 +1,26 @@ +.AuthorsList { + .action { + display: flex; + align-items: center; + justify-content: center; + min-height: 8rem; + } + + .loading { + @include font-size(1.4rem); + + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + width: 100%; + flex-direction: row; + opacity: 0.5; + + .icon { + position: relative; + width: 18px; + height: 18px; + } + } +} diff --git a/src/components/AuthorsList/AuthorsList.tsx b/src/components/AuthorsList/AuthorsList.tsx new file mode 100644 index 00000000..236a586e --- /dev/null +++ b/src/components/AuthorsList/AuthorsList.tsx @@ -0,0 +1,90 @@ +import { clsx } from 'clsx' +import { For, Show, createEffect, createSignal } from 'solid-js' +import { useFollowing } from '../../context/following' +import { useLocalize } from '../../context/localize' +import { apiClient } from '../../graphql/client/core' +import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors' +import { AuthorBadge } from '../Author/AuthorBadge' +import { InlineLoader } from '../InlineLoader' +import { Button } from '../_shared/Button' +import styles from './AuthorsList.module.scss' + +type Props = { + class?: string + query: 'shouts' | 'followers' +} + +const PAGE_SIZE = 20 +export const AuthorsList = (props: Props) => { + const { t } = useLocalize() + const { isOwnerSubscribed } = useFollowing() + const [loading, setLoading] = createSignal(false) + const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 }) + const { authorsByShouts, authorsByFollowers } = useAuthorsStore() + + const fetchAuthors = async (queryType: 'shouts' | 'followers', page: number) => { + setLoading(true) + const offset = PAGE_SIZE * page + const result = await apiClient.loadAuthorsBy({ + by: { order: queryType }, + limit: PAGE_SIZE, + offset: offset, + }) + + if (queryType === 'shouts') { + setAuthorsByShouts((prev) => [...prev, ...result]) + } else { + setAuthorsByFollowers((prev) => [...prev, ...result]) + } + setLoading(false) + return result + } + + const loadMoreAuthors = () => { + const queryType = props.query + const nextPage = currentPage()[queryType] + 1 + fetchAuthors(queryType, nextPage).then(() => + setCurrentPage({ ...currentPage(), [queryType]: nextPage }), + ) + } + + createEffect(() => { + const queryType = props.query + if ( + currentPage()[queryType] === 0 && + (authorsByShouts().length === 0 || authorsByFollowers().length === 0) + ) { + loadMoreAuthors() + } + }) + + const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers()) + + return ( +
+ + {(author) => ( +
+
+ +
+
+ )} +
+
+ +
+
+ ) +} diff --git a/src/components/AuthorsList/index.ts b/src/components/AuthorsList/index.ts new file mode 100644 index 00000000..4187ebae --- /dev/null +++ b/src/components/AuthorsList/index.ts @@ -0,0 +1 @@ +export { AuthorsList } from './AuthorsList' diff --git a/src/components/InlineLoader/InlineLoader.module.scss b/src/components/InlineLoader/InlineLoader.module.scss new file mode 100644 index 00000000..dc90c7bd --- /dev/null +++ b/src/components/InlineLoader/InlineLoader.module.scss @@ -0,0 +1,18 @@ +.InlineLoader { + @include font-size(1.4rem); + + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + width: 100%; + flex-direction: row; + opacity: 0.5; + + .icon { + position: relative; + width: 18px; + height: 18px; + } + +} diff --git a/src/components/InlineLoader/InlineLoader.tsx b/src/components/InlineLoader/InlineLoader.tsx new file mode 100644 index 00000000..6f36ff4e --- /dev/null +++ b/src/components/InlineLoader/InlineLoader.tsx @@ -0,0 +1,20 @@ +import { clsx } from 'clsx' +import { useLocalize } from '../../context/localize' +import { Loading } from '../_shared/Loading' +import styles from './InlineLoader.module.scss' + +type Props = { + class?: string +} + +export const InlineLoader = (props: Props) => { + const { t } = useLocalize() + return ( +
+
+ +
+
{t('Loading')}
+
+ ) +} diff --git a/src/components/InlineLoader/index.ts b/src/components/InlineLoader/index.ts new file mode 100644 index 00000000..c94c5a50 --- /dev/null +++ b/src/components/InlineLoader/index.ts @@ -0,0 +1 @@ +export { InlineLoader } from './InlineLoader' diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx deleted file mode 100644 index 6275621f..00000000 --- a/src/components/Views/AllAuthors.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import type { Author } from '../../graphql/schema/core.gen' - -import { Meta } from '@solidjs/meta' -import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' - -import { useFollowing } from '../../context/following' -import { useLocalize } from '../../context/localize' -import { useRouter } from '../../stores/router' -import { loadAuthors, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' -import { dummyFilter } from '../../utils/dummyFilter' -import { getImageUrl } from '../../utils/getImageUrl' -import { scrollHandler } from '../../utils/scroll' -import { authorLetterReduce, translateAuthor } from '../../utils/translate' -import { AuthorBadge } from '../Author/AuthorBadge' -import { Loading } from '../_shared/Loading' -import { SearchField } from '../_shared/SearchField' - -import styles from './AllAuthors.module.scss' - -type AllAuthorsPageSearchParams = { - by: '' | 'name' | 'shouts' | 'followers' -} - -type Props = { - authors: Author[] - isLoaded: boolean -} - -const PAGE_SIZE = 20 - -export const AllAuthorsView = (props: Props) => { - const { t, lang } = useLocalize() - const ALPHABET = - lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@'] - const [offsetByShouts, setOffsetByShouts] = createSignal(0) - const [offsetByFollowers, setOffsetByFollowers] = createSignal(0) - const { searchParams, changeSearchParams } = useRouter() - const { sortedAuthors } = useAuthorsStore({ - authors: props.authors, - sortBy: searchParams().by || 'name', - }) - - const [searchQuery, setSearchQuery] = createSignal('') - const offset = searchParams()?.by === 'shouts' ? offsetByShouts : offsetByFollowers - createEffect(() => { - let by = searchParams().by - if (by) { - setAuthorsSort(by) - } else { - by = 'name' - changeSearchParams({ by }) - } - }) - - const loadMoreByShouts = async () => { - await loadAuthors({ by: { order: 'shouts_stat' }, limit: PAGE_SIZE, offset: offsetByShouts() }) - setOffsetByShouts((o) => o + PAGE_SIZE) - } - const loadMoreByFollowers = async () => { - await loadAuthors({ by: { order: 'followers_stat' }, limit: PAGE_SIZE, offset: offsetByFollowers() }) - setOffsetByFollowers((o) => o + PAGE_SIZE) - } - - const isStatsLoaded = createMemo(() => sortedAuthors()?.some((author) => author.stat)) - - createEffect(async () => { - if (!isStatsLoaded()) { - await loadMoreByShouts() - await loadMoreByFollowers() - } - }) - - const showMore = async () => - await { - shouts: loadMoreByShouts, - followers: loadMoreByFollowers, - }[searchParams().by]() - - const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { - return sortedAuthors().reduce( - (acc, author) => authorLetterReduce(acc, author, lang()), - {} as { [letter: string]: Author[] }, - ) - }) - - const { isOwnerSubscribed } = useFollowing() - - const sortedKeys = createMemo(() => { - const keys = Object.keys(byLetter()) - keys.sort() - keys.push(keys.shift()) - return keys - }) - - const filteredAuthors = createMemo(() => { - return dummyFilter(sortedAuthors(), searchQuery(), lang()) - }) - - const ogImage = getImageUrl('production/image/logo_image.png') - const ogTitle = t('Authors') - const description = t('List of authors of the open editorial community') - - return ( -
- - - - - - - - - - - }> -
-
-
-

{t('Authors')}

-

{t('Subscribe who you like to tune your personal feed')}

- - - -
-
- - 0}> - - - - - {(letter) => ( -
-

{letter}

-
-
-
-
- - {(author) => ( -
-
- {translateAuthor(author, lang())} - - {author.stat.shouts} - -
-
- )} -
-
-
-
-
-
- )} -
-
- - - - {(author) => ( -
-
- -
-
- )} -
-
- - PAGE_SIZE + offset() && searchParams().by !== 'name'}> -
-
- -
-
-
-
-
-
-
- ) -} diff --git a/src/components/Views/AllAuthors.module.scss b/src/components/Views/AllAuthors/AllAuthors.module.scss similarity index 99% rename from src/components/Views/AllAuthors.module.scss rename to src/components/Views/AllAuthors/AllAuthors.module.scss index 94d4302c..63188b2b 100644 --- a/src/components/Views/AllAuthors.module.scss +++ b/src/components/Views/AllAuthors/AllAuthors.module.scss @@ -81,3 +81,5 @@ overflow-x: auto; } } + + diff --git a/src/components/Views/AllAuthors/AllAuthors.tsx b/src/components/Views/AllAuthors/AllAuthors.tsx new file mode 100644 index 00000000..c1723891 --- /dev/null +++ b/src/components/Views/AllAuthors/AllAuthors.tsx @@ -0,0 +1,177 @@ +import type { Author } from '../../../graphql/schema/core.gen' + +import { Meta } from '@solidjs/meta' +import { clsx } from 'clsx' +import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' + +import { useLocalize } from '../../../context/localize' +import { useRouter } from '../../../stores/router' +import { setAuthorsSort, useAuthorsStore } from '../../../stores/zine/authors' +import { getImageUrl } from '../../../utils/getImageUrl' +import { scrollHandler } from '../../../utils/scroll' +import { authorLetterReduce, translateAuthor } from '../../../utils/translate' + +import { AuthorsList } from '../../AuthorsList' +import { Loading } from '../../_shared/Loading' +import { SearchField } from '../../_shared/SearchField' + +import styles from './AllAuthors.module.scss' + +type AllAuthorsPageSearchParams = { + by: '' | 'name' | 'shouts' | 'followers' +} + +type Props = { + authors: Author[] + isLoaded: boolean +} + +export const AllAuthors = (props: Props) => { + const { t, lang } = useLocalize() + const ALPHABET = + lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@'] + const { searchParams, changeSearchParams } = useRouter() + const { sortedAuthors } = useAuthorsStore({ + authors: props.authors, + sortBy: searchParams().by || 'name', + }) + + const [searchQuery, setSearchQuery] = createSignal('') + + createEffect(() => { + let by = searchParams().by + if (by) { + setAuthorsSort(by) + } else { + by = 'name' + changeSearchParams({ by }) + } + }) + + const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { + return sortedAuthors().reduce( + (acc, author) => authorLetterReduce(acc, author, lang()), + {} as { [letter: string]: Author[] }, + ) + }) + + const sortedKeys = createMemo(() => { + const keys = Object.keys(byLetter()) + keys.sort() + keys.push(keys.shift()) + return keys + }) + + const ogImage = getImageUrl('production/image/logo_image.png') + const ogTitle = t('Authors') + const description = t('List of authors of the open editorial community') + + return ( +
+ + + + + + + + + + + }> +
+
+
+

{t('Authors')}

+

{t('Subscribe who you like to tune your personal feed')}

+ +
+
+ + + + + {(letter) => ( +
+

{letter}

+
+
+
+
+ + {(author) => ( +
+
+ {translateAuthor(author, lang())} + + {author.stat.shouts} + +
+
+ )} +
+
+
+
+
+
+ )} +
+
+ }> + + +
+
+
+ ) +} diff --git a/src/components/Views/AllAuthors/index.ts b/src/components/Views/AllAuthors/index.ts new file mode 100644 index 00000000..13e92537 --- /dev/null +++ b/src/components/Views/AllAuthors/index.ts @@ -0,0 +1 @@ +export { AllAuthors } from './AllAuthors' diff --git a/src/components/_shared/InviteMembers/InviteMembers.module.scss b/src/components/_shared/InviteMembers/InviteMembers.module.scss index 8710a65a..0e9f8964 100644 --- a/src/components/_shared/InviteMembers/InviteMembers.module.scss +++ b/src/components/_shared/InviteMembers/InviteMembers.module.scss @@ -50,24 +50,6 @@ } } - .loading { - @include font-size(1.4rem); - - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - width: 100%; - flex-direction: row; - opacity: 0.5; - - .icon { - position: relative; - width: 18px; - height: 18px; - } - } - .teaser { min-height: 300px; display: flex; diff --git a/src/components/_shared/InviteMembers/InviteMembers.tsx b/src/components/_shared/InviteMembers/InviteMembers.tsx index 0458c7a5..3eca7cdc 100644 --- a/src/components/_shared/InviteMembers/InviteMembers.tsx +++ b/src/components/_shared/InviteMembers/InviteMembers.tsx @@ -12,6 +12,7 @@ import { Button } from '../Button' import { DropdownSelect } from '../DropdownSelect' import { Loading } from '../Loading' +import { InlineLoader } from '../../InlineLoader' import styles from './InviteMembers.module.scss' type InviteAuthor = Author & { selected: boolean } @@ -62,7 +63,7 @@ export const InviteMembers = (props: Props) => { return authors?.slice(start, end) } - const [pages, _infiniteScrollLoader, { end }] = createInfiniteScroll(fetcher) + const [pages, setEl, { end }] = createInfiniteScroll(fetcher) createEffect( on( @@ -158,11 +159,8 @@ export const InviteMembers = (props: Props) => { )} -
-
- -
-
{t('Loading')}
+
void}> +
diff --git a/src/pages/allAuthors.page.tsx b/src/pages/allAuthors.page.tsx index 87a427b2..7079e8af 100644 --- a/src/pages/allAuthors.page.tsx +++ b/src/pages/allAuthors.page.tsx @@ -1,8 +1,8 @@ import type { PageProps } from './types' -import { createSignal, onMount } from 'solid-js' +import { createEffect, createSignal, onMount } from 'solid-js' -import { AllAuthorsView } from '../components/Views/AllAuthors' +import { AllAuthors } from '../components/Views/AllAuthors/' import { PageLayout } from '../components/_shared/PageLayout' import { useLocalize } from '../context/localize' import { loadAllAuthors } from '../stores/zine/authors' @@ -23,7 +23,7 @@ export const AllAuthorsPage = (props: PageProps) => { return ( - + ) } diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index db3f0627..c046737a 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -6,6 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen' import { byStat } from '../../utils/sortby' export type AuthorsSortBy = 'shouts' | 'name' | 'followers' +type SortedAuthorsSetter = (prev: Author[]) => Author[] const [sortAllBy, setSortAllBy] = createSignal('name') @@ -13,6 +14,11 @@ export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy) const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({}) const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({}) +const [authorsByShouts, setSortedAuthorsByShout] = createSignal([]) +const [authorsByFollowers, setSortedAuthorsByFollowers] = createSignal([]) + +export const setAuthorsByShouts = (authors: SortedAuthorsSetter) => setSortedAuthorsByShout(authors) +export const setAuthorsByFollowers = (authors: SortedAuthorsSetter) => setSortedAuthorsByFollowers(authors) const sortedAuthors = createLazyMemo(() => { const authors = Object.values(authorEntities()) @@ -108,5 +114,5 @@ export const useAuthorsStore = (initialState: InitialState = {}) => { } addAuthors([...(initialState.authors || [])]) - return { authorEntities, sortedAuthors, authorsByTopic } + return { authorEntities, sortedAuthors, authorsByTopic, authorsByShouts, authorsByFollowers } }