diff --git a/package.json b/package.json index 4374ead7..3dcb901d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@types/swiper": "^5.4.2", "autosize": "^4.0.2", "axios": "^0.21.1", "body-scroll-lock": "^2.6.4", @@ -34,6 +35,7 @@ "redux-saga": "^1.1.1", "resize-sensor": "^0.0.6", "sticky-sidebar": "^3.3.1", + "swiper": "^6.5.0", "throttle-debounce": "^2.1.0", "typescript": "^4.0.5", "uuid4": "^1.1.4", diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx index 94acddc4..2ac1119a 100644 --- a/src/components/editors/AudioEditor/index.tsx +++ b/src/components/editors/AudioEditor/index.tsx @@ -8,6 +8,8 @@ import { selectUploads } from '~/redux/uploads/selectors'; import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; import styles from './styles.module.scss'; import { NodeEditorProps } from '~/redux/node/types'; +import { useNodeImages } from '~/utils/hooks/node/useNodeImages'; +import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios'; const mapStateToProps = selectUploads; const mapDispatchToProps = { @@ -17,10 +19,7 @@ const mapDispatchToProps = { type IProps = ReturnType & typeof mapDispatchToProps & NodeEditorProps; const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) => { - const images = useMemo( - () => data.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), - [data.files] - ); + const images = useNodeImages(data); const pending_images = useMemo( () => @@ -30,10 +29,7 @@ const AudioEditorUnconnected: FC = ({ data, setData, temp, statuses }) = [temp, statuses] ); - const audios = useMemo( - () => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), - [data.files] - ); + const audios = useNodeAudios(data); const pending_audios = useMemo( () => diff --git a/src/components/node/NodeImageSwiperBlock/index.tsx b/src/components/node/NodeImageSwiperBlock/index.tsx index c9dd4e79..6e9c0a95 100644 --- a/src/components/node/NodeImageSwiperBlock/index.tsx +++ b/src/components/node/NodeImageSwiperBlock/index.tsx @@ -1,7 +1,83 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { INodeComponentProps } from '~/redux/node/constants'; +import SwiperCore, { A11y, Pagination, SwiperOptions } from 'swiper'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/swiper.scss'; +import 'swiper/components/navigation/navigation.scss'; +import 'swiper/components/pagination/pagination.scss'; +import 'swiper/components/scrollbar/scrollbar.scss'; -interface IProps {} +import styles from './styles.module.scss'; +import { useNodeImages } from '~/utils/hooks/node/useNodeImages'; +import { getURL } from '~/utils/dom'; +import { PRESETS } from '~/constants/urls'; +import SwiperClass from 'swiper/types/swiper-class'; -const NodeImageSwiperBlock: FC = () =>
SWIPER
; +SwiperCore.use([Pagination, A11y]); + +interface IProps extends INodeComponentProps {} + +const breakpoints: SwiperOptions['breakpoints'] = { + 599: { + spaceBetween: 20, + }, +}; + +const NodeImageSwiperBlock: FC = ({ node }) => { + const [controlledSwiper, setControlledSwiper] = useState(undefined); + + const images = useNodeImages(node); + + const updateSwiper = useCallback(() => { + if (!controlledSwiper) return; + + controlledSwiper.updateSlides(); + controlledSwiper.updateSize(); + controlledSwiper.update(); + }, [controlledSwiper]); + + const resetSwiper = useCallback(() => { + if (!controlledSwiper) return; + controlledSwiper.slideTo(0); + }, [controlledSwiper]); + + useEffect(() => { + updateSwiper(); + resetSwiper(); + }, [images, updateSwiper, resetSwiper]); + + if (!images?.length) { + return null; + } + + return ( +
+ + {images.map(file => ( + + {node.title} + + ))} + +
+ ); +}; export { NodeImageSwiperBlock }; diff --git a/src/components/node/NodeImageSwiperBlock/styles.module.scss b/src/components/node/NodeImageSwiperBlock/styles.module.scss new file mode 100644 index 00000000..cc72b54c --- /dev/null +++ b/src/components/node/NodeImageSwiperBlock/styles.module.scss @@ -0,0 +1,66 @@ +@import "~/styles/variables.scss"; + +.wrapper { + border-radius: $radius; + display: flex; + align-items: center; + justify-content: center; + + :global(.swiper-pagination) { + left: 50%; + bottom: $gap * 2; + transform: translate(-50%, 0); + background: darken($comment_bg, 4%); + width: auto; + padding: 5px 10px; + border-radius: 10px; + font: $font_10_semibold; + } + + :global(.swiper-container) { + width: 100vw; + } +} + +.slide { + text-align: center; + text-transform: uppercase; + font: $font_32_bold; + display: flex; + border-radius: $radius; + align-items: center; + justify-content: center; + width: auto; + max-width: 100vw; + opacity: 1; + filter: brightness(50%) saturate(0.5); + transition: opacity 0.5s, filter 1s, transform 1s; + padding-bottom: $gap * 1.5; + padding-top: $gap; + + &:global(.swiper-slide-active) { + opacity: 1; + filter: brightness(100%); + } + + @include tablet { + padding-bottom: 0; + } +} + +.image { + max-height: calc(100vh - 70px - 70px); + max-width: 100%; + border-radius: $radius; + + box-shadow: transparentize(black, 0.9) 0 10px 5px 4px, + transparentize(black, 0.7) 0 5px 5px, + transparentize(white, 0.95) 0 -1px 2px, + transparentize(white, 0.95) 0 -1px; + + @include tablet { + padding-bottom: 0; + max-height: 100vh; + border-radius: 0; + } +} diff --git a/src/containers/main/Container/styles.module.scss b/src/containers/main/Container/styles.module.scss index 8da5ff08..cbf85b78 100644 --- a/src/containers/main/Container/styles.module.scss +++ b/src/containers/main/Container/styles.module.scss @@ -4,4 +4,9 @@ width: 100%; max-width: $content_width; margin: auto; + padding: 0 $gap; + + @include tablet { + padding: 0; + } } diff --git a/src/containers/main/MainLayout/styles.module.scss b/src/containers/main/MainLayout/styles.module.scss index 918061ea..79ec32f8 100644 --- a/src/containers/main/MainLayout/styles.module.scss +++ b/src/containers/main/MainLayout/styles.module.scss @@ -2,17 +2,12 @@ .wrapper { width: 100%; - padding: 0 $gap; display: flex; flex-direction: column; box-sizing: border-box; align-items: center; justify-content: flex-start; flex: 1; - - @include tablet { - padding: 0; - } } .content { diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index ffad260c..b7664ba8 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,6 +1,5 @@ import { FC } from 'react'; import { IComment, INode, ValueOf } from '../types'; -import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock'; import { NodeTextBlock } from '~/components/node/NodeTextBlock'; import { NodeAudioBlock } from '~/components/node/NodeAudioBlock'; import { NodeVideoBlock } from '~/components/node/NodeVideoBlock'; @@ -14,6 +13,7 @@ import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadB import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton'; import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types'; import { EditorFiller } from '~/components/editors/EditorFiller'; +import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock'; const prefix = 'NODE.'; export const NODE_ACTIONS = { @@ -81,7 +81,7 @@ export type INodeComponentProps = { export type INodeComponents = Record, FC>; export const NODE_HEADS: INodeComponents = { - [NODE_TYPES.IMAGE]: NodeImageSlideBlock, + [NODE_TYPES.IMAGE]: NodeImageSwiperBlock, }; export const NODE_COMPONENTS: INodeComponents = { diff --git a/src/utils/hooks/node/useNodeAudios.ts b/src/utils/hooks/node/useNodeAudios.ts new file mode 100644 index 00000000..7ece487f --- /dev/null +++ b/src/utils/hooks/node/useNodeAudios.ts @@ -0,0 +1,9 @@ +import { INode } from '~/redux/types'; +import { useMemo } from 'react'; +import { UPLOAD_TYPES } from '~/redux/uploads/constants'; + +export const useNodeAudios = (node: INode) => { + return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [ + node.files, + ]); +}; diff --git a/src/utils/hooks/node/useNodeImages.ts b/src/utils/hooks/node/useNodeImages.ts new file mode 100644 index 00000000..4f6b71d5 --- /dev/null +++ b/src/utils/hooks/node/useNodeImages.ts @@ -0,0 +1,9 @@ +import { INode } from '~/redux/types'; +import { useMemo } from 'react'; +import { UPLOAD_TYPES } from '~/redux/uploads/constants'; + +export const useNodeImages = (node: INode) => { + return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [ + node.files, + ]); +}; diff --git a/src/utils/node.ts b/src/utils/node.ts index d8254eda..f00d006c 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -1,10 +1,8 @@ import { USER_ROLES } from '~/redux/auth/constants'; -import { ICommentGroup, IFile, INode } from '~/redux/types'; +import { ICommentGroup, INode } from '~/redux/types'; import { IUser } from '~/redux/auth/types'; import { path } from 'ramda'; import { NODE_TYPES } from '~/redux/node/constants'; -import { useMemo } from 'react'; -import { UPLOAD_TYPES } from '~/redux/uploads/constants'; export const canEditNode = (node: Partial, user: Partial): boolean => path(['role'], user) === USER_ROLES.ADMIN || @@ -21,11 +19,3 @@ export const canStarNode = (node: Partial, user: Partial): boolean node.type === NODE_TYPES.IMAGE && path(['role'], user) && path(['role'], user) === USER_ROLES.ADMIN; - -export const useNodeImages = (node: INode): IFile[] => { - return useMemo( - () => - (node && node.files && node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || [], - [node.files] - ); -}; diff --git a/yarn.lock b/yarn.lock index 5dfb8655..f0c085db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1761,6 +1761,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/swiper@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@types/swiper/-/swiper-5.4.2.tgz#ff206cf5aea787f580b5dd9b466b4bcb8e0442f3" + integrity sha512-/7MaVDZ8ltMCZb6yfg1HWBRjwFjy9ytKpuPSZfNTrxpkQCaGQZdpceDSqKaSfGmJcVF0NcBFRsGTStyytV7grw== + "@types/testing-library__jest-dom@^5.9.1": version "5.9.5" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0" @@ -4084,6 +4089,13 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" +dom7@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331" + integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g== + dependencies: + ssr-window "^3.0.0-alpha.1" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -10449,6 +10461,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssr-window@^3.0.0, ssr-window@^3.0.0-alpha.1: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37" + integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA== + ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -10789,6 +10806,14 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +swiper@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-6.5.0.tgz#4ca2243b44fccef47ee28199377666607d8c5141" + integrity sha512-cSx1SpfgrHlgwku++3Ce3cjPBpXgB7P+bGik5S3+F+j6ID0NUeV6qtmedFdr3C8jXR/W+TJPVNIT9fH/cwVAiA== + dependencies: + dom7 "^3.0.0" + ssr-window "^3.0.0" + symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"