slider-as-wrapper

This commit is contained in:
tonyrewin 2022-11-27 20:02:04 +03:00
parent 7a97e8303d
commit a3f63e0da0
12 changed files with 185 additions and 125 deletions

View File

@ -11,6 +11,7 @@ import { deleteReaction } from '../../stores/zine/reactions'
import { formatDate } from '../../utils'
import { SharePopup } from './SharePopup'
import stylesHeader from '../Nav/Header.module.scss'
import Userpic from '../Author/Userpic'
export default (props: {
level?: number
@ -40,16 +41,11 @@ export default (props: {
<Show
when={!props.compact}
fallback={
<div class={styles.commentDetails}>
<a href={`/author/${comment()?.createdBy?.slug}`}>
@{(comment()?.createdBy || { name: 'anonymous' }).name}
</a>
<div class={styles.commentArticle}>
<Icon name="reply-arrow" />
<a href={`#comment-${comment()?.id}`}>
#{(comment()?.shout || { title: 'Lorem ipsum titled' }).title}
</a>
</div>
<div>
<Userpic user={comment().createdBy as Author} isBig={false} isAuthorsList={false} />
<small class={styles.commentArticle}>
<a href={`#comment-${comment()?.id}`}>{comment()?.shout.title || ''}</a>
</small>
</div>
}
>
@ -141,9 +137,9 @@ export default (props: {
<textarea name="reply" id="reply" rows="5"></textarea>
<div class={styles.replyFormControls}>
<button class="button button--light" onClick={() => setIsReplyVisible(false)}>
Отмена
{t('Cancel')}
</button>
<button class="button">Отправить</button>
<button class="button">{t('Send')}</button>
</div>
</form>
</Show>

View File

@ -14,6 +14,7 @@ import { clsx } from 'clsx'
import { CommentsTree } from './CommentsTree'
import { useSession } from '../../context/session'
import VideoPlayer from './VideoPlayer'
import Slider from '../_shared/Slider'
interface ArticleProps {
article: Shout
@ -29,13 +30,7 @@ interface MediaItem {
const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
return (
<>
<Switch
fallback={
<picture>
<source src={props.media.url} />
</picture>
}
>
<Switch fallback={<a href={props.media.url}>{t('Cannot show this media type')}</a>}>
<Match when={props.kind === 'audio'}>
<div>
<h5>{props.media.title}</h5>
@ -116,7 +111,21 @@ export const FullArticle = (props: ArticleProps) => {
<div class={styles.shoutCover} style={{ 'background-image': `url('${props.article.cover}')` }} />
</div>
<Show when={media()}>
<Show
when={media() && props.article.layout !== 'image'}
fallback={
<Slider>
<For each={media() || []}>
{(m: MediaItem) => (
<>
<img src={m.url || m.pic} alt={m.title} />
<div innerHTML={m.body} />
</>
)}
</For>
</Slider>
}
>
<div class="media-items">
<For each={media() || []}>
{(m: MediaItem) => (
@ -148,7 +157,7 @@ export const FullArticle = (props: ArticleProps) => {
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem)}>
<Icon name="eye" class={styles.icon} />
<sup>{props.article.stat?.viewed}</sup>
{props.article.stat?.viewed}
</div>
</Show>

View File

@ -33,9 +33,6 @@ export const AuthorCard = (props: AuthorCardProps) => {
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
)
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
const bio = createMemo(() => {
return props.caption || props.author.bio || t('Our regular contributor')
})
const name = () => {
return props.author.name === 'Дискурс' && locale() !== 'ru'
@ -76,7 +73,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
<div
class={styles.authorAbout}
classList={{ 'text-truncate': props.truncateBio }}
innerHTML={props.caption || bio()}
innerHTML={props.author.bio}
></div>
</Show>
</div>

View File

@ -51,6 +51,7 @@ export default (props: UserpicProps) => {
src={props.user.userpic || '/icons/user-default.svg'}
alt={props.user.name || ''}
classList={{ anonymous: !props.user.userpic }}
loading="lazy"
/>
}
>

View File

@ -20,6 +20,11 @@
a {
border: none;
}
.icon {
height: 1.2em;
width: 1.2em;
}
}
.shoutCardWithBorder {

View File

@ -13,9 +13,10 @@ import { t } from '../../utils/intl'
import { Row3 } from '../Feed/Row3'
import { Row2 } from '../Feed/Row2'
import { Beside } from '../Feed/Beside'
import Slider from '../Feed/Slider'
import Slider from '../_shared/Slider'
import { Row1 } from '../Feed/Row1'
import styles from '../../styles/Topic.module.scss'
import { ArticleCard } from '../Feed/Card'
export const PRERENDERED_ARTICLES_COUNT = 21
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
@ -106,7 +107,21 @@ export const LayoutShoutsPage = (props: PageProps) => {
<ModeSwitcher />
<Row1 article={sortedArticles()[0]} />
<Row2 articles={sortedArticles().slice(1, 3)} />
<Slider title={title()} articles={sortedArticles().slice(5, 11)} />
<Slider title={title()}>
<For each={sortedArticles().slice(5, 11)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside
beside={sortedArticles()[12]}
title={t('Top viewed')}

View File

@ -1,14 +1,14 @@
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'
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
import { AuthorsSortBy, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
import { useRouter } from '../../stores/router'
import styles from '../../styles/AllTopics.module.scss'
import { AuthorCard } from '../Author/Card'
import { clsx } from 'clsx'
import { useSession } from '../../context/session'
import { locale } from '../../stores/ui'
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'
@ -17,19 +17,20 @@ type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'followers'
}
type Props = {
type AllAuthorsViewProps = {
authors: Author[]
}
const PAGE_SIZE = 20
const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ']
export const AllAuthorsView = (props: Props) => {
export const AllAuthorsView = (props: AllAuthorsViewProps) => {
const [limit, setLimit] = createSignal(PAGE_SIZE)
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
const { searchParams, changeSearchParam } = useRouter()
const [filterResults, setFilterResults] = createSignal<Author[]>([])
const { sortedAuthors } = useAuthorsStore({
authors: props.authors,
sortBy: searchParams().by || 'name'
sortBy: (searchParams().by || 'shouts') as AuthorsSortBy
})
const { session } = useSession()
@ -41,13 +42,11 @@ export const AllAuthorsView = (props: Props) => {
}
})
createEffect(() => {
setAuthorsSort(searchParams().by || 'shouts')
setFilteredAuthors(sortedAuthors())
setAuthorsSort((searchParams().by || 'shouts') as AuthorsSortBy)
setFilterResults(sortedAuthors())
setLimit(PAGE_SIZE)
})
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce((acc, author) => {
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
@ -64,6 +63,39 @@ export const AllAuthorsView = (props: Props) => {
return keys
})
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
// eslint-disable-next-line sonarjs/cognitive-complexity
const filterAuthors = (value) => {
/* very stupid filter by string algorithm with no deps */
let q = value.toLowerCase()
if (q.length > 0) {
setFilterResults([])
if (locale() === 'ru') q = translit(q, 'ru')
const aaa: Author[] = sortedAuthors()
sortedAuthors().forEach((author) => {
let flag = false
author.slug.split('-').forEach((w) => {
if (w.startsWith(q)) flag = true
})
if (!flag) {
let wrds: string = author.name.toLowerCase()
if (locale() === 'ru') wrds = translit(wrds, 'ru')
wrds.split(' ').forEach((w: string) => {
if (w.startsWith(q)) flag = true
})
}
if (!flag && aaa.includes(author)) {
const idx = aaa.indexOf(author)
aaa.splice(idx, 1)
}
})
setFilterResults(aaa)
}
}
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const AllAuthorsHead = () => (
<div class="row">
@ -76,54 +108,24 @@ export const AllAuthorsView = (props: Props) => {
<a href="/authors?by=shouts">{t('By shouts')}</a>
</li>
<li classList={{ selected: searchParams().by === 'followers' }}>
<a href="/authors?by=followers">{t('By rating')}</a>
<a href="/authors?by=followers">{t('By popularity')}</a>
</li>
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
<li classList={{ selected: searchParams().by === 'name' }}>
<a href="/authors?by=name">{t('By name')}</a>
</li>
<li class="view-switcher__search">
<Show when={searchParams().by !== 'name'}>
<li class="view-switcher__search">
<SearchField onChange={filterAuthors} />
</li>
</li>
</Show>
</ul>
</div>
</div>
)
const [filteredAuthors, setFilteredAuthors] = createSignal<Author[]>([])
// eslint-disable-next-line sonarjs/cognitive-complexity
const filterAuthors = (value) => {
/* very stupid search algorithm with no deps */
let q = value.toLowerCase()
if (q.length > 0) {
setFilteredAuthors([])
if (locale() === 'ru') q = translit(q, 'ru')
const aaa: Author[] = sortedAuthors()
sortedAuthors().forEach((a) => {
let flag = false
a.slug.split('-').forEach((w) => {
if (w.startsWith(q)) flag = true
})
if (!flag) {
let wrds: string = a.name.toLowerCase()
if (locale() === 'ru') wrds = translit(wrds, 'ru')
wrds.split(' ').forEach((w: string) => {
if (w.startsWith(q)) flag = true
})
}
if (!flag && aaa.includes(a)) {
const idx = aaa.indexOf(a)
aaa.splice(idx, 1)
}
})
setFilteredAuthors(aaa)
}
}
return (
<div class={clsx(styles.allTopicsPage, 'wide-container')}>
<Show when={sortedAuthors().length > 0 || filteredAuthors().length > 0}>
<Show when={sortedAuthors().length > 0}>
<div class="shift-content">
<AllAuthorsHead />
@ -132,7 +134,7 @@ export const AllAuthorsView = (props: Props) => {
<div class="col-lg-10 col-xl-9">
<ul class={clsx('nodash', styles.alphabet)}>
<For each={ALPHABET}>
{(letter: string, index) => (
{(letter, index) => (
<li>
<Show when={letter in byLetter()} fallback={letter}>
<a
@ -176,27 +178,22 @@ export const AllAuthorsView = (props: Props) => {
</For>
</Show>
<Show when={searchParams().by && searchParams().by !== 'name'}>
<div class={clsx(styles.stats, 'row')}>
<div class="col-lg-10 col-xl-9">
<For each={filteredAuthors().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>
</div>
</div>
<Show when={searchParams().by && searchParams().by !== 'title'}>
<For each={filterResults().slice(0, limit())}>
{(author) => (
<>
<AuthorCard
author={author}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
/>
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={author.stat} />
</>
)}
</For>
</Show>
<Show when={sortedAuthors().length > limit() && searchParams().by !== 'name'}>

View File

@ -8,7 +8,7 @@ import { Row1 } from '../Feed/Row1'
import Hero from '../Discours/Hero'
import { Beside } from '../Feed/Beside'
import RowShort from '../Feed/RowShort'
import Slider from '../Feed/Slider'
import Slider from '../_shared/Slider'
import Group from '../Feed/Group'
import type { Shout, Topic } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
@ -18,6 +18,7 @@ import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { locale } from '../../stores/ui'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import { ArticleCard } from '../Feed/Card'
type HomeProps = {
randomTopics: Topic[]
@ -120,7 +121,21 @@ export const HomeView = (props: HomeProps) => {
wrapper={'author'}
/>
<Slider title={t('Top month articles')} articles={topMonthArticles()} />
<Slider title={t('Top month articles')}>
<For each={topMonthArticles()}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Row2 articles={sortedArticles().slice(10, 12)} />
@ -132,7 +147,21 @@ export const HomeView = (props: HomeProps) => {
{randomLayout()}
<Slider title={t('Favorite')} articles={topArticles()} />
<Slider title={t('Favorite')}>
<For each={topArticles()}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside
beside={sortedArticles()[20]}

View File

@ -13,8 +13,9 @@ import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import { clsx } from 'clsx'
import Slider from '../Feed/Slider'
import Slider from '../_shared/Slider'
import { Row1 } from '../Feed/Row1'
import { ArticleCard } from '../Feed/Card'
type TopicsPageSearchParams = {
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
@ -122,7 +123,21 @@ export const TopicView = (props: TopicProps) => {
wrapper={'author'}
/>
<Slider title={title()} articles={sortedArticles().slice(5, 11)} />
<Slider title={title()}>
<For each={sortedArticles().slice(5, 11)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside
beside={sortedArticles()[12]}
@ -134,12 +149,21 @@ export const TopicView = (props: TopicProps) => {
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
<Row1 article={sortedArticles()[15]} />
<Slider
title={title()}
articles={sortedArticles().slice(16, 22)}
slidesPerView={3}
isCardsWithCover={false}
/>
<Slider slidesPerView={3} title={title()}>
<For each={sortedArticles().slice(16, 22)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: false,
nodate: true
}}
/>
)}
</For>
</Slider>
<Row3 articles={sortedArticles().slice(23, 26)} />
<Row2 articles={sortedArticles().slice(26, 28)} />

View File

@ -1,4 +1,3 @@
import { ArticleCard } from './Card'
import { Swiper, Navigation, Pagination } from 'swiper'
import type { SwiperOptions } from 'swiper'
import 'swiper/scss'
@ -6,14 +5,15 @@ import 'swiper/scss/navigation'
import 'swiper/scss/pagination'
import './Slider.scss'
import type { Shout } from '../../graphql/types.gen'
import { createEffect, createMemo, createSignal, Show, For } from 'solid-js'
import { Icon } from '../_shared/Icon'
import { createEffect, createMemo, createSignal, Show, For, JSX } from 'solid-js'
import { Icon } from './Icon'
interface SliderProps {
title?: string
articles: Shout[]
articles?: Shout[]
slidesPerView?: number
isCardsWithCover?: boolean
children?: JSX.Element
}
export default (props: SliderProps) => {
@ -66,21 +66,7 @@ export default (props: SliderProps) => {
<h2 class="col-12">{props.title}</h2>
<Show when={!!articles()}>
<div class="swiper" classList={{ 'cards-with-cover': isCardsWithCover }} ref={el}>
<div class="swiper-wrapper">
<For each={articles()}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: isCardsWithCover,
nodate: true
}}
/>
)}
</For>
</div>
<div class="swiper-wrapper">{props.children}</div>
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
<Icon name="slider-arrow" class={'icon'} />
</div>

View File

@ -15,6 +15,7 @@
"By alphabet": "По алфавиту",
"By authors": "По авторам",
"By name": "По имени",
"By popularity": "По популярности",
"By rating": "По популярности",
"By relevance": "По релевантности",
"By shouts": "По публикациям",