beautiful all-topics and all-authors, some icons

This commit is contained in:
tonyrewin 2022-11-27 11:15:03 +03:00
parent ae91ce27df
commit 9b4cede871
14 changed files with 102 additions and 92 deletions

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20.5,15.1a1,1,0,0,0-1.34.45A8,8,0,1,1,12,4a7.93,7.93,0,0,1,7.16,4.45,1,1,0,0,0,1.8-.9,10,10,0,1,0,0,8.9A1,1,0,0,0,20.5,15.1ZM21,11H11.41l2.3-2.29a1,1,0,1,0-1.42-1.42l-4,4a1,1,0,0,0-.21.33,1,1,0,0,0,0,.76,1,1,0,0,0,.21.33l4,4a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42L11.41,13H21a1,1,0,0,0,0-2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12.59,13l-2.3,2.29a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0l4-4a1,1,0,0,0,.21-.33,1,1,0,0,0,0-.76,1,1,0,0,0-.21-.33l-4-4a1,1,0,1,0-1.42,1.42L12.59,11H3a1,1,0,0,0,0,2ZM12,2A10,10,0,0,0,3,7.55a1,1,0,0,0,1.8.9A8,8,0,1,1,12,20a7.93,7.93,0,0,1-7.16-4.45,1,1,0,0,0-1.8.9A10,10,0,1,0,12,2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@ -1,3 +1,4 @@
<svg width="14" height="19" viewBox="0 0 14 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0H14V18.6667L7 14.9333L0 18.6667V0ZM2 2V15.3333L7 12.6667L12 15.3333V2H2Z" fill="#141414"/> <path
d="M16,2H8A3,3,0,0,0,5,5V21a1,1,0,0,0,.5.87,1,1,0,0,0,1,0L12,18.69l5.5,3.18A1,1,0,0,0,18,22a1,1,0,0,0,.5-.13A1,1,0,0,0,19,21V5A3,3,0,0,0,16,2Zm1,17.27-4.5-2.6a1,1,0,0,0-1,0L7,19.27V5A1,1,0,0,1,8,4h8a1,1,0,0,1,1,1Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 304 B

View File

@ -1,3 +1,4 @@
<svg width="14" height="19" viewBox="0 0 14 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H14V18.6667L7 14.9333L0 18.6667V0ZM2 2V15.3333L7 12.6667L12 15.3333V2H2Z" fill="#141414"/> <path fill-rule="evenodd" clip-rule="evenodd" fill="#141414"
d="M16,2H8A3,3,0,0,0,5,5V21a1,1,0,0,0,.5.87,1,1,0,0,0,1,0L12,18.69l5.5,3.18A1,1,0,0,0,18,22a1,1,0,0,0,.5-.13A1,1,0,0,0,19,21V5A3,3,0,0,0,16,2Zm1,17.27-4.5-2.6a1,1,0,0,0-1,0L7,19.27V5A1,1,0,0,1,8,4h8a1,1,0,0,1,1,1Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 359 B

View File

@ -122,7 +122,7 @@ export default (props: {
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
trigger={ trigger={
<button class={clsx(styles.commentControl, styles.commentControlShare)}> <button class={clsx(styles.commentControl, styles.commentControlShare)}>
<Icon name="share" class={styles.icon} /> <Icon name="share-new" class={styles.icon} />
{t('Share')} {t('Share')}
</button> </button>
} }

View File

@ -5,20 +5,28 @@ import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import styles from '../../styles/Article.module.scss' import styles from '../../styles/Article.module.scss'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import { createEffect, createMemo, createSignal, onMount, Suspense } from 'solid-js' import { createMemo, createSignal, onMount } from 'solid-js'
import type { Reaction } from '../../graphql/types.gen' import type { Reaction } from '../../graphql/types.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { byCreated, byStat } from '../../utils/sortby'
import { Loading } from '../Loading'
const ARTICLE_COMMENTS_PAGE_SIZE = 50 const ARTICLE_COMMENTS_PAGE_SIZE = 50
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
export const CommentsTree = (props: { shout: string; reactions?: Reaction[] }) => { export const CommentsTree = (props: { shout: string; reactions?: Reaction[] }) => {
const [getCommentsPage, setCommentsPage] = createSignal(0) const [getCommentsPage, setCommentsPage] = createSignal(0)
const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt')
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false) const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { session } = useSession() const { session } = useSession()
const { sortedReactions, loadReactionsBy } = useReactionsStore({ reactions: props.reactions }) const { sortedReactions, loadReactionsBy } = useReactionsStore({ reactions: props.reactions })
const reactions = createMemo<Reaction[]>(() => sortedReactions()) // .filter(r => r.shout.slug === props.shout) ) const reactions = createMemo<Reaction[]>(() =>
sortedReactions()
.sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
.filter((r) => r.shout.slug === props.shout)
)
const loadMore = async () => { const loadMore = async () => {
try { try {
const page = getCommentsPage() const page = getCommentsPage()
@ -44,29 +52,38 @@ export const CommentsTree = (props: { shout: string; reactions?: Reaction[] }) =
onMount(async () => await loadMore()) onMount(async () => await loadMore())
return ( return (
<> <>
<Show when={reactions()}> <Show when={!isCommentsLoading()} fallback={<Loading />}>
<div class={styles.commentsHeaderWrapper}> <div class={styles.commentsHeaderWrapper}>
<h2 id="comments" class={styles.commentsHeader}> <h2 id="comments" class={styles.commentsHeader}>
{t('Comments')} {reactions().length.toString() || ''} {t('Comments')} {reactions().length.toString() || ''}
</h2> </h2>
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}> <ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
<li class="selected"> <li classList={{ selected: commentsOrder() === 'createdAt' || !commentsOrder() }}>
<a href="#">По порядку</a> <a
href="#"
onClick={(ev) => {
ev.preventDefault()
setCommentsOrder('createdAt')
}}
>
По порядку
</a>
</li> </li>
<li> <li classList={{ selected: commentsOrder() === 'rating' }}>
<a href="#">По рейтингу</a> <a
href="#"
onClick={(ev) => {
ev.preventDefault()
setCommentsOrder('rating')
}}
>
По рейтингу
</a>
</li> </li>
</ul> </ul>
</div> </div>
<form class={styles.commentForm}>
<div class="pretty-form__item">
<input type="text" id="new-comment" placeholder="Коментарий" />
<label for="new-comment">Коментарий</label>
</div>
</form>
<For each={reactions()}> <For each={reactions()}>
{(reaction: Reaction) => ( {(reaction: Reaction) => (
<Comment <Comment
@ -84,7 +101,14 @@ export const CommentsTree = (props: { shout: string; reactions?: Reaction[] }) =
<Show <Show
when={!session()?.user?.slug} when={!session()?.user?.slug}
fallback={<textarea class={styles.writeComment} rows="1" placeholder={t('Write comment')} />} fallback={
<form class={styles.commentForm}>
<div class="pretty-form__item">
<input type="text" id="new-comment" placeholder={t('Write comment')} />
<label for="new-comment">{t('Write comment')}</label>
</div>
</form>
}
> >
<div class={styles.commentWarning} id="comments"> <div class={styles.commentWarning} id="comments">
{t('To leave a comment you please')} {t('To leave a comment you please')}

View File

@ -99,7 +99,7 @@ export const FullArticle = (props: ArticleProps) => {
setIsSharePopupVisible(isVisible) setIsSharePopupVisible(isVisible)
}} }}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
trigger={<Icon name="share" class={styles.icon} />} trigger={<Icon name="share-new" class={styles.icon} />}
/> />
</div> </div>
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>

View File

@ -37,14 +37,14 @@ export const Beside = (props: BesideProps) => {
<Show when={props.wrapper === 'author'}> <Show when={props.wrapper === 'author'}>
<a href="/authors"> <a href="/authors">
{t('All authors')} {t('All authors')}
<Icon name="arrow-right" class={styles.icon} /> <Icon name="arrow-circle-right" class={styles.icon} />
</a> </a>
</Show> </Show>
<Show when={props.wrapper === 'topic'}> <Show when={props.wrapper === 'topic'}>
<a href="/topics"> <a href="/topics">
{t('All topics')} {t('All topics')}
<Icon name="arrow-right" class={styles.icon} /> <Icon name="arrow-circle-right" class={styles.icon} />
</a> </a>
</Show> </Show>
</div> </div>

View File

@ -12,7 +12,10 @@
padding: 0 divide($container-padding-x, 2); padding: 0 divide($container-padding-x, 2);
} }
} }
.icon {
height: 1.2em;
width: 1.2em;
}
a:hover { a:hover {
.icon { .icon {
filter: invert(1); filter: invert(1);

View File

@ -127,7 +127,7 @@ export const Header = (props: Props) => {
setIsSharePopupVisible(isVisible) setIsSharePopupVisible(isVisible)
}} }}
containerCssClass={styles.control} containerCssClass={styles.control}
trigger={<Icon name="share-outline" class={styles.icon} />} trigger={<Icon name="share-new" class={styles.icon} />}
/> />
<a href={getPagePath(router, 'inbox')} class={styles.control}> <a href={getPagePath(router, 'inbox')} class={styles.control}>
<Icon name="comments-outline" class={styles.icon} /> <Icon name="comments-outline" class={styles.icon} />

View File

@ -41,7 +41,8 @@ export const AllAuthorsView = (props: Props) => {
} }
}) })
createEffect(() => { createEffect(() => {
setAuthorsSort(searchParams().by || 'name') setAuthorsSort(searchParams().by || 'shouts')
setFilteredAuthors(sortedAuthors())
setLimit(PAGE_SIZE) setLimit(PAGE_SIZE)
}) })
@ -81,23 +82,23 @@ export const AllAuthorsView = (props: Props) => {
<a href="/authors?by=name">{t('By name')}</a> <a href="/authors?by=name">{t('By name')}</a>
</li> </li>
<li class="view-switcher__search"> <li class="view-switcher__search">
<SearchField onChange={searchAuthors} /> <li class="view-switcher__search">
<SearchField onChange={filterAuthors} />
</li>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
) )
const [searchResults, setSearchResults] = createSignal<Author[]>([]) const [filteredAuthors, setFilteredAuthors] = createSignal<Author[]>([])
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const searchAuthors = (value) => { const filterAuthors = (value) => {
/* very stupid search algorithm with no deps */ /* very stupid search algorithm with no deps */
let q = value.toLowerCase() let q = value.toLowerCase()
if (q.length > 0) { if (q.length > 0) {
console.debug(q) setFilteredAuthors([])
setSearchResults([])
if (locale() === 'ru') q = translit(q, 'ru') if (locale() === 'ru') q = translit(q, 'ru')
const aaa: Author[] = [] const aaa: Author[] = sortedAuthors()
sortedAuthors().forEach((a) => { sortedAuthors().forEach((a) => {
let flag = false let flag = false
a.slug.split('-').forEach((w) => { a.slug.split('-').forEach((w) => {
@ -112,16 +113,17 @@ export const AllAuthorsView = (props: Props) => {
}) })
} }
if (flag && !aaa.includes(a)) aaa.push(a) if (!flag && aaa.includes(a)) {
const idx = aaa.indexOf(a)
aaa.splice(idx, 1)
}
}) })
setFilteredAuthors(aaa)
setSearchResults((sr: Author[]) => [...sr, ...aaa])
changeSearchParam('by', '')
} }
} }
return ( return (
<div class={clsx(styles.allTopicsPage, 'wide-container')}> <div class={clsx(styles.allTopicsPage, 'wide-container')}>
<Show when={sortedAuthors().length > 0 || searchResults().length > 0}> <Show when={sortedAuthors().length > 0 || filteredAuthors().length > 0}>
<div class="shift-content"> <div class="shift-content">
<AllAuthorsHead /> <AllAuthorsHead />
@ -174,29 +176,10 @@ export const AllAuthorsView = (props: Props) => {
</For> </For>
</Show> </Show>
<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}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)}
</For>
</Show>
<Show when={searchParams().by && searchParams().by !== 'name'}> <Show when={searchParams().by && searchParams().by !== 'name'}>
<div class={clsx(styles.stats, 'row')}> <div class={clsx(styles.stats, 'row')}>
<div class="col-lg-10 col-xl-9"> <div class="col-lg-10 col-xl-9">
<For each={sortedAuthors().slice(0, limit())}> <For each={filteredAuthors().slice(0, limit())}>
{(author) => ( {(author) => (
<> <>
<AuthorCard <AuthorCard
@ -216,7 +199,7 @@ export const AllAuthorsView = (props: Props) => {
</div> </div>
</Show> </Show>
<Show when={searchParams().by !== 'name' && sortedAuthors().length > limit()}> <Show when={sortedAuthors().length > limit() && searchParams().by !== 'name'}>
<div class="row"> <div class="row">
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}> <div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}> <button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>

View File

@ -43,6 +43,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
}) })
createEffect(() => { createEffect(() => {
setTopicsSort(searchParams().by || 'shouts') setTopicsSort(searchParams().by || 'shouts')
setFilterResults(sortedTopics())
setLimit(PAGE_SIZE) setLimit(PAGE_SIZE)
}) })
@ -65,17 +66,16 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || ''))
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchResults, setSearchResults] = createSignal<Topic[]>([])
const [filterResults, setFilterResults] = createSignal<Topic[]>([])
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const searchTopics = (value) => { const filterTopics = (value) => {
/* very stupid search algorithm with no deps */ /* very stupid filter by string algorithm with no deps */
let q = value.toLowerCase() let q = value.toLowerCase()
if (q.length > 0) { if (q.length > 0) {
console.debug(q) setFilterResults([])
setSearchResults([])
if (locale() === 'ru') q = translit(q, 'ru') if (locale() === 'ru') q = translit(q, 'ru')
const ttt: Topic[] = [] const ttt: Topic[] = sortedTopics()
sortedTopics().forEach((topic) => { sortedTopics().forEach((topic) => {
let flag = false let flag = false
topic.slug.split('-').forEach((w) => { topic.slug.split('-').forEach((w) => {
@ -90,11 +90,12 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
}) })
} }
if (flag && !ttt.includes(topic)) ttt.push(topic) if (!flag && ttt.includes(topic)) {
const idx = ttt.indexOf(topic)
ttt.splice(idx, 1)
}
}) })
setFilterResults(ttt)
setSearchResults((sr: Topic[]) => [...sr, ...ttt])
changeSearchParam('by', '')
} }
} }
@ -114,9 +115,11 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
<li classList={{ selected: searchParams().by === 'title' }}> <li classList={{ selected: searchParams().by === 'title' }}>
<a href="/topics?by=title">{t('By title')}</a> <a href="/topics?by=title">{t('By title')}</a>
</li> </li>
<li class="view-switcher__search"> <Show when={searchParams().by !== 'title'}>
<SearchField onChange={searchTopics} /> <li class="view-switcher__search">
</li> <SearchField onChange={filterTopics} />
</li>
</Show>
</ul> </ul>
</div> </div>
</div> </div>
@ -127,7 +130,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
<div class="shift-content"> <div class="shift-content">
<AllTopicsHead /> <AllTopicsHead />
<Show when={sortedTopics().length > 0 || searchResults().length > 0}> <Show when={filterResults().length > 0}>
<Show when={searchParams().by === 'title'}> <Show when={searchParams().by === 'title'}>
<div class="col-lg-10 col-xl-9"> <div class="col-lg-10 col-xl-9">
<ul class={clsx('nodash', styles.alphabet)}> <ul class={clsx('nodash', styles.alphabet)}>
@ -173,21 +176,8 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
</For> </For>
</Show> </Show>
<Show when={searchResults().length > 1}>
<For each={searchResults().slice(0, limit())}>
{(topic) => (
<TopicCard
topic={topic}
compact={false}
subscribed={subscribed(topic.slug)}
showPublications={true}
/>
)}
</For>
</Show>
<Show when={searchParams().by && searchParams().by !== 'title'}> <Show when={searchParams().by && searchParams().by !== 'title'}>
<For each={sortedTopics().slice(0, limit())}> <For each={filterResults().slice(0, limit())}>
{(topic) => ( {(topic) => (
<> <>
<TopicCard <TopicCard
@ -202,7 +192,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
</For> </For>
</Show> </Show>
<Show when={sortedTopics().length > limit()}> <Show when={sortedTopics().length > limit() && searchParams().by !== 'title'}>
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10 offset-md-1')}> <div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10 offset-md-1')}>
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}> <button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
{t('Load more')} {t('Load more')}

View File

@ -12,9 +12,9 @@ export default gql`
# id # id
# kind # kind
#} #}
#shout { shout {
# slug slug
#} }
createdBy { createdBy {
name name
slug slug

View File

@ -139,6 +139,7 @@ img {
vertical-align: baseline; vertical-align: baseline;
.icon { .icon {
height: 1.5em;
display: inline-block; display: inline-block;
margin-right: 0.2em; margin-right: 0.2em;
transition: filter 0.2s; transition: filter 0.2s;
@ -183,6 +184,7 @@ img {
.icon { .icon {
opacity: 0.4; opacity: 0.4;
height: 2rem;
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {