1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-05-07 10:36:41 +07:00

Merge branch 'feature/9-tiny-slider' into develop

This commit is contained in:
Fedor Katurov 2020-10-29 20:15:09 +07:00
commit 0362854543
13 changed files with 238 additions and 25 deletions
src
components
containers/FullWidth
node
NodeImageSlideBlock
NodeImageTinySlider
containers
redux/node
styles
utils

View file

@ -0,0 +1,52 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styles from './styles.module.scss';
import ResizeSensor from 'resize-sensor';
interface IProps {
onRefresh?: (width: number) => void;
}
const FullWidth: FC<IProps> = ({ children, onRefresh }) => {
const sample = useRef<HTMLDivElement>(null);
const [clientWidth, setClientWidth] = useState(document.documentElement.clientWidth);
const style = useMemo(() => {
if (!sample.current) return { display: 'none' };
const { width } = sample.current.getBoundingClientRect();
const { clientWidth } = document.documentElement;
onRefresh(clientWidth);
return {
width: clientWidth,
transform: `translate(-${(clientWidth - width) / 2}px, 0)`,
};
}, [sample.current, clientWidth, onRefresh]);
const onResize = useCallback(() => setClientWidth(document.documentElement.clientWidth), []);
useEffect(() => {
if (!sample.current) return;
window.addEventListener('resize', onResize);
new ResizeSensor(document.body, onResize);
return () => {
window.removeEventListener('resize', onResize);
ResizeSensor.detach(document.body, onResize);
};
}, []);
return (
<div className={styles.wrap}>
<div className={styles.slider} style={style}>
{children}
</div>
<div className={styles.sample} ref={sample} />
</div>
);
};
export { FullWidth };

View file

@ -0,0 +1,10 @@
.sample {
width: 100%;
display: block;
background: green;
height: 0;
}
.slider {
display: block;
}

View file

@ -8,6 +8,7 @@ import { PRESETS } from '~/constants/urls';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { throttle } from 'throttle-debounce';
import { Icon } from '~/components/input/Icon';
import { useArrows } from '~/utils/hooks/keys';
interface IProps extends INodeComponentProps {}
@ -239,29 +240,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
images,
]);
const onKeyDown = useCallback(
event => {
if (
(event.target.tagName && ['TEXTAREA', 'INPUT'].includes(event.target.tagName)) ||
is_modal_shown
)
return;
switch (event.key) {
case 'ArrowLeft':
return onPrev();
case 'ArrowRight':
return onNext();
}
},
[onNext, onPrev, is_modal_shown]
);
useEffect(() => {
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
useArrows(onNext, onPrev, is_modal_shown);
useEffect(() => {
setOffset(0);

View file

@ -0,0 +1,93 @@
import React, { FC, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { INodeComponentProps } from '~/redux/node/constants';
import { FullWidth } from '~/components/containers/FullWidth';
import { useNodeImages } from '~/utils/node';
import { getURL } from '~/utils/dom';
import { PRESETS } from '~/constants/urls';
import TinySlider from 'tiny-slider-react';
import styles from './styles.module.scss';
import { TinySliderInstance, TinySliderSettings } from 'tiny-slider';
import { useArrows } from '~/utils/hooks/keys';
const settings: TinySliderSettings & { center: boolean } = {
nav: false,
mouseDrag: true,
gutter: 10,
center: true,
lazyload: true,
items: 1,
edgePadding: 150,
loop: false,
arrowKeys: false,
// prevButton: false,
// nextButton: false,
autoHeight: true,
swipeAngle: 45,
responsive: {
0: {
edgePadding: 10,
gutter: 40,
},
768: {
edgePadding: 50,
},
1024: {
edgePadding: 150,
},
},
};
const NodeImageTinySlider: FC<INodeComponentProps> = ({ node }) => {
const ref = useRef(null);
const slides = useRef<HTMLDivElement[]>([]);
const images = useNodeImages(node);
const [current, setCurrent] = useState(0);
const [height, setHeight] = useState(images[0]?.metadata?.height || 0);
const onResize = useCallback(() => {
if (!ref.current) return;
ref.current.slider.refresh();
const el = slides.current[current];
if (!el) return;
const { height } = el.getBoundingClientRect();
setHeight(height);
}, [ref.current, slides.current, current]);
const onIndexChanged = useCallback(({ index }) => {
setCurrent(index || 0);
}, []);
useEffect(() => {
setCurrent(0);
}, [node.id]);
useEffect(onResize, [slides, current]);
const onNext = useCallback(() => {
if (!ref.current || images.length <= 1 || current === images.length - 1) return;
ref.current.slider.goTo(current + 1);
}, [ref.current, current, images]);
const onPrev = useCallback(() => {
if (!ref.current || images.length <= 1 || current === 0) return;
ref.current.slider.goTo(current - 1);
}, [ref.current, current, images]);
useArrows(onNext, onPrev, false);
return (
<FullWidth onRefresh={onResize}>
<div className={styles.slider} style={{ height }}>
<TinySlider settings={settings} ref={ref} onTransitionEnd={onIndexChanged}>
{images.map((image, i) => (
<div className={styles.slide} key={image.id} ref={el => (slides.current[i] = el)}>
<img src={getURL(image, PRESETS['1600'])} key={image.url} onLoad={onResize} />
</div>
))}
</TinySlider>
</div>
</FullWidth>
);
};
export { NodeImageTinySlider };

View file

@ -0,0 +1,22 @@
.slider {
padding-bottom: 15px;
overflow: hidden;
transition: height 0.25s;
:global(.tns-controls) {
display: none;
}
}
.slide {
align-items: center;
display: inline-flex;
justify-content: center;
text-align: center;
img {
max-height: calc(100vh - 140px);
max-width: 100%;
border-radius: $radius;
}
}

View file

@ -25,6 +25,7 @@ const Component: FC<IProps> = ({ modal: { is_shown } }) => {
<div>
<BlurWrapper is_blurred={is_shown}>
<PageCover />
<MainLayout>
<Modal />
<Sprites />

View file

@ -16,6 +16,7 @@ import { Filler } from '~/components/containers/Filler';
import { modalShowPhotoswipe } from '../modal/actions';
import { IEditorComponentProps } from '~/redux/node/types';
import { EditorFiller } from '~/components/editors/EditorFiller';
import { NodeImageTinySlider } from '~/components/node/NodeImageTinySlider';
const prefix = 'NODE.';
export const NODE_ACTIONS = {
@ -90,7 +91,8 @@ export type INodeComponentProps = {
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
export const NODE_HEADS: INodeComponents = {
[NODE_TYPES.IMAGE]: NodeImageSlideBlock,
// [NODE_TYPES.IMAGE]: NodeImageSlideBlock,
[NODE_TYPES.IMAGE]: NodeImageTinySlider,
};
export const NODE_COMPONENTS: INodeComponents = {

View file

@ -2,6 +2,8 @@
html {
min-height: 100vh;
box-sizing: border-box;
overflow: auto;
}
body {

23
src/utils/hooks/keys.ts Normal file
View file

@ -0,0 +1,23 @@
import { useCallback, useEffect } from 'react';
export const useArrows = (onNext: () => void, onPrev: () => void, locked) => {
const onKeyDown = useCallback(
event => {
if ((event.target.tagName && ['TEXTAREA', 'INPUT'].includes(event.target.tagName)) || locked)
return;
switch (event.key) {
case 'ArrowLeft':
return onPrev();
case 'ArrowRight':
return onNext();
}
},
[onNext, onPrev, locked]
);
useEffect(() => {
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
};

View file

@ -1,8 +1,10 @@
import { USER_ROLES } from '~/redux/auth/constants';
import { INode, IComment, ICommentGroup } from '~/redux/types';
import { INode, IComment, ICommentGroup, IFile } from '~/redux/types';
import { IUser } from '~/redux/auth/types';
import path from 'ramda/es/path';
import { NODE_TYPES } from '~/redux/node/constants';
import { useMemo } from 'react';
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN ||
@ -19,3 +21,11 @@ export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): 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]
);
};