Use Solid Swiper (#293)

Use Solid Swiper
This commit is contained in:
Ilya Y 2023-11-02 13:34:38 +03:00 committed by GitHub
parent f1e68f219c
commit 891b9ec5f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 131 additions and 485 deletions

View File

@ -20,7 +20,7 @@ import { AudioHeader } from './AudioHeader'
import { Popover } from '../_shared/Popover'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { Icon } from '../_shared/Icon'
import { SolidSwiper } from '../_shared/SolidSwiper'
import { ImageSwiper } from '../_shared/SolidSwiper'
import styles from './Article.module.scss'
import { CardTopic } from '../Feed/CardTopic'
import { createPopper } from '@popperjs/core'
@ -331,7 +331,7 @@ export const FullArticle = (props: Props) => {
<div class="wide-container">
<div class="row">
<div class="col-md-20 offset-md-2">
<SolidSwiper images={media()} />
<ImageSwiper images={media()} />
</div>
</div>
</div>

View File

@ -12,7 +12,7 @@ import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader'
import { AudioUploader } from '../Editor/AudioUploader'
import { slugify } from '../../utils/slugify'
import { SolidSwiper } from '../_shared/SolidSwiper'
import { ImageSwiper } from '../_shared/SolidSwiper'
import { DropArea } from '../_shared/DropArea'
import { LayoutType, MediaItem } from '../../pages/types'
import { clone } from '../../utils/clone'
@ -384,7 +384,7 @@ export const EditView = (props: Props) => {
</div>
<Show when={props.shout.layout === 'image'}>
<SolidSwiper
<ImageSwiper
editorMode={true}
images={mediaItems()}
onImageChange={handleMediaChange}

View File

@ -8,10 +8,8 @@ import { Row1 } from '../Feed/Row1'
import Hero from '../Discours/Hero'
import { Beside } from '../Feed/Beside'
import RowShort from '../Feed/RowShort'
import { Slider } from '../_shared/Slider'
import Group from '../Feed/Group'
import type { Shout } from '../../graphql/types.gen'
import { useTopicsStore } from '../../stores/zine/topics'
import {
loadShouts,
@ -22,8 +20,8 @@ import {
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize'
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
type Props = {
shouts: Shout[]
@ -130,21 +128,7 @@ export const HomeView = (props: Props) => {
/>
<Show when={topMonthArticles()}>
<Slider title={t('Top month articles')}>
<For each={topMonthArticles()}>
{(a) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
</Show>
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
@ -162,21 +146,7 @@ export const HomeView = (props: Props) => {
{randomLayout()}
<Show when={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>
<ArticleCardSwiper title={t('Favorite')} slides={topArticles()} />
</Show>
<Beside

View File

View File

@ -13,10 +13,10 @@ import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
import { clsx } from 'clsx'
import { Slider } from '../_shared/Slider'
import { Row1 } from '../Feed/Row1'
import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize'
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
type TopicsPageSearchParams = {
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
@ -136,21 +136,7 @@ export const TopicView = (props: TopicProps) => {
wrapper={'author'}
/>
<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>
<ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
<Beside
beside={sortedArticles()[12]}
@ -163,22 +149,7 @@ export const TopicView = (props: TopicProps) => {
<Row1 article={sortedArticles()[15]} />
<Show when={sortedArticles().length > 15}>
<Slider slidesPerView={3}>
<For each={sortedArticles().slice(16, 22)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: false,
nodate: true
}}
/>
)}
</For>
</Slider>
<ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
<Row3 articles={sortedArticles().slice(23, 26)} />
<Row2 articles={sortedArticles().slice(26, 28)} />
</Show>

View File

@ -1,253 +0,0 @@
.swiper-slide {
min-height: 0 !important;
.cards-with-cover & {
height: 0 !important;
padding-top: 100%;
@include media-breakpoint-up(sm) {
padding-top: 56.2% !important;
}
@include media-breakpoint-up(md) {
padding-top: 35% !important;
}
img {
height: 100%;
left: 0;
object-fit: cover;
object-position: center;
position: absolute;
top: 0;
width: 100%;
}
}
}
.slider-arrow-prev,
.slider-arrow-next {
align-items: center;
display: flex;
cursor: pointer;
height: 100%;
position: absolute;
outline: none;
border: 0;
transform: translate(0);
top: 0;
width: 21%;
z-index: 1;
@include media-breakpoint-down(md) {
width: 8%;
}
&::after {
color: #fff;
}
&:hover {
.icon {
opacity: 0.5;
}
}
.icon {
height: 36px;
opacity: 1;
transition: opacity 0.2s;
width: 22px;
}
}
.slider-arrow-prev {
background: linear-gradient(to left, rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 90%) 100%);
justify-content: flex-start;
left: 0;
&::after {
margin-left: 5rem;
}
.icon {
margin-left: 5rem;
@include media-breakpoint-down(md) {
margin-left: 25%;
}
}
}
.slider-arrow-next {
background: linear-gradient(to left, rgb(0 0 0 / 90%) 0%, rgb(0 0 0 / 0%) 100%);
justify-content: flex-end;
right: 0;
&::after {
margin-right: 5rem;
}
.icon {
margin-right: 5rem;
transform: rotate(180deg);
@include media-breakpoint-down(md) {
margin-right: 25%;
}
}
}
.swiper--page-gallery {
padding-bottom: 4rem;
.swiper-wrapper {
align-items: center;
}
.swiper-slide {
display: flex;
justify-content: center;
}
.swiper-slide__inner {
display: flex;
flex-direction: column;
justify-content: center;
img {
display: block;
height: auto;
margin: 0 auto;
max-height: 80vh;
width: auto;
position: relative;
z-index: 11;
}
}
.slider-arrow-prev,
.slider-arrow-next {
background: rgb(0 0 0 / 20%);
width: 5rem;
}
.slider-arrow-next .icon {
margin-right: 2rem;
}
.slider-arrow-prev .icon {
margin-left: 2rem;
}
.swiper-slide-active {
.swiper-lazy-preloader {
display: none;
}
}
}
.thumbs-container {
@include media-breakpoint-up(md) {
padding-left: 3.2rem;
}
}
.swiper--thumbs {
@include media-breakpoint-up(md) {
max-height: 80vh;
min-width: 100px;
padding: 0 !important;
width: 100px !important;
}
.swiper-slide {
cursor: pointer;
height: 80px;
opacity: 0.5;
width: 100px;
@include media-breakpoint-up(md) {
height: 52px;
width: auto;
}
img {
height: 100%;
object-fit: cover;
object-position: center;
width: 100%;
}
}
.swiper-slide-thumb-active {
opacity: 1;
}
.swiper-slide__inner {
height: 100%;
flex: 1;
}
.swiper-lazy-preloader,
.image-description {
display: none;
}
}
.sliders-container {
@include media-breakpoint-up(md) {
display: flex;
}
}
.swiper-pagination {
background: #141414;
bottom: 0;
font-size: 1.2rem;
font-weight: bold;
left: auto;
right: 0;
padding: 1rem;
width: auto;
}
.uploadPreview {
background: unset;
position: relative;
padding: 0 40px;
.sliders-container {
position: relative;
}
.swiper {
background: unset;
.swiper-wrapper {
min-height: 400px;
}
}
.slider-arrow-next,
.slider-arrow-prev {
background: none;
filter: invert(1);
width: 40px;
max-height: 540px;
.icon {
margin: auto;
width: 12px;
height: 20px;
}
}
//
//.slider-arrow-prev {
// margin-left: -40px;
//}
//.slider-arrow-next {
// margin-right: -40px;
//}
}

View File

@ -1,157 +0,0 @@
//TODO: Replace with SolidSwiper.tsx
import { Swiper, Navigation, Pagination, Thumbs } from 'swiper'
import type { SwiperOptions } from 'swiper'
import 'swiper/scss'
import 'swiper/scss/navigation'
import 'swiper/scss/pagination'
import 'swiper/scss/thumbs'
import './Slider.scss'
import { createEffect, createSignal, JSX, on, Show } from 'solid-js'
import { Icon } from '../Icon'
import { clsx } from 'clsx'
interface Props {
title?: string
slidesPerView?: number
isCardsWithCover?: boolean
children?: JSX.Element
isPageGallery?: boolean
hasThumbs?: boolean
variant?: 'uploadPreview'
slideIndex?: (value: number) => void
}
export const Slider = (props: Props) => {
let el: HTMLDivElement | undefined
let thumbsEl: HTMLDivElement | undefined
let nextEl: HTMLDivElement | undefined
let prevEl: HTMLDivElement | undefined
const isCardsWithCover = typeof props.isCardsWithCover === 'boolean' ? props.isCardsWithCover : true
const [swiper, setSwiper] = createSignal<Swiper>()
const [swiperThumbs, setSwiperThumbs] = createSignal<Swiper>()
const opts: SwiperOptions = {
observer: true,
observeParents: true,
roundLengths: true,
loop: true,
centeredSlides: true,
slidesPerView: 1,
modules: [Navigation, Pagination, Thumbs],
speed: 500,
on: {
slideChange: () => {
if (swiper()) {
props.slideIndex(swiper().realIndex || 0)
}
}
},
navigation: { nextEl, prevEl },
breakpoints: {
768: {
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
spaceBetween: isCardsWithCover ? 8 : 26
},
992: {
slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666,
spaceBetween: isCardsWithCover ? 8 : 52
}
},
thumbs: {
swiper: swiperThumbs()
}
}
createEffect(() => {
if (props.hasThumbs && !!thumbsEl) {
setSwiperThumbs(
new Swiper(thumbsEl, {
slidesPerView: 'auto',
modules: [Thumbs],
roundLengths: true,
spaceBetween: 20,
freeMode: true,
breakpoints: {
768: {
direction: 'vertical'
}
}
})
)
}
})
createEffect(() => {
if (!swiper() && !!el) {
if (swiperThumbs()) {
opts.thumbs = {
swiper: swiperThumbs()
}
opts.pagination = {
el: '.swiper-pagination',
type: 'fraction'
}
}
setSwiper(new Swiper(el, opts))
swiper().update()
}
})
createEffect(() => {
swiper().update()
})
return (
<div class={clsx('floor', 'floor--important', props.variant)}>
<div class="wide-container">
<div class="row">
<Show when={props.title}>
<h2 class="col-24">{props.title}</h2>
</Show>
<div class="sliders-container">
<div
class={clsx('swiper')}
classList={{
'cards-with-cover': isCardsWithCover,
'swiper--page-gallery': props.isPageGallery
}}
ref={el}
>
<div class="swiper-wrapper">{props.children}</div>
<Show when={!(props.variant === 'uploadPreview')}>
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
<Icon name="slider-arrow" class={'icon'} />
</div>
<div class="slider-arrow-prev" ref={prevEl} onClick={() => swiper()?.slidePrev()}>
<Icon name="slider-arrow" class={'icon'} />
</div>
</Show>
{/*<div class="swiper-pagination" ref={pagEl} />*/}
</div>
<Show when={props.hasThumbs}>
<div class="thumbs-container">
<div class="swiper swiper--thumbs" ref={thumbsEl}>
<div class="swiper-wrapper">{props.children}</div>
</div>
</div>
</Show>
</div>
</div>
</div>
<Show when={props.variant === 'uploadPreview'}>
<div class="slider-arrow-next" ref={nextEl} onClick={() => swiper()?.slideNext()}>
<Icon name="slider-arrow" class={'icon'} />
</div>
<div class="slider-arrow-prev" ref={prevEl} onClick={() => swiper()?.slidePrev()}>
<Icon name="slider-arrow" class={'icon'} />
</div>
</Show>
</div>
)
}

View File

@ -1 +0,0 @@
export { Slider } from './Slider'

View File

@ -0,0 +1,97 @@
import { createSignal, For, Show } from 'solid-js'
import { Icon } from '../Icon'
import { register } from 'swiper/element/bundle'
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
import { SwiperRef } from './swiper'
import { clsx } from 'clsx'
import styles from './Swiper.module.scss'
import { Shout } from '../../../graphql/types.gen'
import { ArticleCard } from '../../Feed/ArticleCard'
type Props = {
slides: Shout[]
slidesPerView?: number
title?: string
}
register()
SwiperCore.use([Pagination, Navigation, Manipulation])
export const ArticleCardSwiper = (props: Props) => {
const [slideIndex, setSlideIndex] = createSignal(0)
const mainSwipeRef: { current: SwiperRef } = { current: null }
const handleSlideChange = () => {
setSlideIndex(mainSwipeRef.current.swiper.activeIndex)
}
return (
<div class={clsx(styles.Swiper, styles.articleMode, styles.ArticleCardSwiper)}>
<Show when={props.title}>
<h2 class={styles.sliderTitle}>{props.title}</h2>
</Show>
<div class={styles.container}>
<Show when={props.slides.length > 0}>
<div class={styles.holder}>
<swiper-container
ref={(el) => (mainSwipeRef.current = el)}
centered-slides={true}
thumbs-swiper={'.thumbSwiper'}
observer={true}
onSlideChange={handleSlideChange}
slides-per-view={props.slidesPerView ?? 1.5}
space-between={52}
breakpoints={{ 768: { spaceBetween: 26 }, 992: { spaceBetween: 52 } }}
loop={true}
speed={800}
autoplay={{
disableOnInteraction: false,
delay: 3000,
pauseOnMouseEnter: true
}}
>
<For each={props.slides}>
{(slide, index) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<swiper-slide virtual-index={index()}>
<ArticleCard
article={slide}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
</swiper-slide>
)}
</For>
</swiper-container>
<div
class={clsx(styles.navigation, styles.prev, {
[styles.disabled]: slideIndex() === 0
})}
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
>
<Icon name="swiper-l-arr" class={styles.icon} />
</div>
<div
class={clsx(styles.navigation, styles.next, {
[styles.disabled]: slideIndex() + 1 === props.slides.length
})}
onClick={() => mainSwipeRef.current.swiper.slideNext()}
>
<Icon name="swiper-r-arr" class={styles.icon} />
</div>
<div class={styles.counter}>
{slideIndex() + 1} / {props.slides.length}
</div>
</div>
</Show>
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { createEffect, createSignal, For, Show, on } from 'solid-js'
import { createEffect, createSignal, For, Show, on, JSXElement } from 'solid-js'
import { MediaItem, UploadedFile } from '../../../pages/types'
import { Icon } from '../Icon'
import { Popover } from '../Popover'
@ -32,7 +32,7 @@ register()
SwiperCore.use([Pagination, Navigation, Manipulation])
export const SolidSwiper = (props: Props) => {
export const ImageSwiper = (props: Props) => {
const { t } = useLocalize()
const [loading, setLoading] = createSignal(false)
const [slideIndex, setSlideIndex] = createSignal(0)
@ -68,7 +68,6 @@ export const SolidSwiper = (props: Props) => {
{ defer: true }
)
)
const handleDropAreaUpload = (value: UploadedFile[]) => {
props.onImagesAdd(composeMediaItems(value))
swipeToUploaded()

View File

@ -13,6 +13,17 @@ $navigation-reserve: 32px;
margin: 2rem 0;
flex-direction: column;
&.ArticleCardSwiper {
margin-bottom: 6rem;
}
.sliderTitle {
@include font-size(4.5rem);
text-align: center;
padding: 4rem 0 0;
}
&.articleMode {
background: var(--background-color-invert);
color: var(--default-color-invert);
@ -114,6 +125,10 @@ $navigation-reserve: 32px;
overflow: hidden;
width: calc(100% - 130px);
@include media-breakpoint-down(sm) {
width: 100%;
}
.counter {
@include font-size(1.2rem);

View File

@ -1 +1 @@
export { SolidSwiper } from './SolidSwiper'
export { ImageSwiper } from './ImageSwiper'

View File

@ -1,5 +1,5 @@
import 'solid-js'
import { SwiperOptions } from 'swiper'
import { SwiperOptions, AutoplayOptions } from 'swiper'
import { SwiperSlideProps } from 'swiper/react'
type Kebab<T extends string, A extends string = ''> = T extends `${infer F}${infer R}`
@ -37,6 +37,11 @@ declare module 'solid-js' {
onSlideChange?: () => void
onBeforeSlideChangeStart?: () => void
class?: string
breakpoints?: {
[width: number]: SwiperOptions
[ratio: string]: SwiperOptions
}
autoplay?: AutoplayOptions | boolean
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface SwiperSlideAttributes extends KebabObjectKeys<SwiperSlideProps> {