mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
added lazy loading to NodeImageSwiperBlock
This commit is contained in:
parent
991f829216
commit
10bb6f01b5
17 changed files with 210 additions and 106 deletions
|
@ -18,7 +18,7 @@ import { Group } from '~/components/containers/Group';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||||
import { UploadType } from '~/constants/uploads';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IComment, IFile } from '~/types';
|
import { IComment, IFile } from '~/types';
|
||||||
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
||||||
import { append, assocPath, path, reduce } from '~/utils/ramda';
|
import { append, assocPath, path, reduce } from '~/utils/ramda';
|
||||||
|
@ -141,7 +141,7 @@ const CommentContent: FC<IProps> = memo(
|
||||||
onClick={() => onShowImageModal(groupped.image, index)}
|
onClick={() => onShowImageModal(groupped.image, index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={getURL(file, ImagePresets['600'])}
|
src={getURL(file, imagePresets['600'])}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { forwardRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Square } from '~/components/common/Square';
|
import { Square } from '~/components/common/Square';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||||
import { getURLFromString } from '~/utils/dom';
|
import { getURLFromString } from '~/utils/dom';
|
||||||
import { DivProps } from '~/utils/types';
|
import { DivProps } from '~/utils/types';
|
||||||
|
@ -14,12 +14,12 @@ interface Props extends DivProps {
|
||||||
url?: string;
|
url?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
preset?: typeof ImagePresets[keyof typeof ImagePresets];
|
preset?: typeof imagePresets[keyof typeof imagePresets];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar = forwardRef<HTMLDivElement, Props>(
|
const Avatar = forwardRef<HTMLDivElement, Props>(
|
||||||
(
|
(
|
||||||
{ url, username, size, className, preset = ImagePresets.avatar, ...rest },
|
{ url, username, size, className, preset = imagePresets.avatar, ...rest },
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IUser } from '~/types/auth';
|
import { IUser } from '~/types/auth';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
||||||
|
|
||||||
const onLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]);
|
const onLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]);
|
||||||
|
|
||||||
const image = getURL(cover, ImagePresets.cover);
|
const image = getURL(cover, imagePresets.cover);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cover || !cover.url || !ref || !ref.current) return;
|
if (!cover || !cover.url || !ref || !ref.current) return;
|
||||||
|
|
||||||
ref.current.src = '';
|
ref.current.src = '';
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
ref.current.src = getURL(cover, ImagePresets.cover);
|
ref.current.src = getURL(cover, imagePresets.cover);
|
||||||
}, [cover]);
|
}, [cover]);
|
||||||
|
|
||||||
if (!cover) return null;
|
if (!cover) return null;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { createContext, FC, useContext, useState } from 'react';
|
||||||
|
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
@ -27,9 +27,11 @@ const PageCoverProvider: FC = ({ children }) => {
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
className={styles.wrap}
|
className={styles.wrap}
|
||||||
style={{ backgroundImage: `url("${getURL(cover, ImagePresets.cover)}")` }}
|
style={{
|
||||||
|
backgroundImage: `url("${getURL(cover, imagePresets.cover)}")`,
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
document.body
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { ChangeEvent, FC, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { UploadSubject, UploadTarget, UploadType } from '~/constants/uploads';
|
import { UploadSubject, UploadTarget, UploadType } from '~/constants/uploads';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useUploader } from '~/hooks/data/useUploader';
|
import { useUploader } from '~/hooks/data/useUploader';
|
||||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||||
import { IEditorComponentProps } from '~/types/node';
|
import { IEditorComponentProps } from '~/types/node';
|
||||||
|
@ -18,10 +18,12 @@ const EditorUploadCoverButton: FC<IProps> = () => {
|
||||||
const { uploadFile, files, pendingImages } = useUploader(
|
const { uploadFile, files, pendingImages } = useUploader(
|
||||||
UploadSubject.Editor,
|
UploadSubject.Editor,
|
||||||
UploadTarget.Nodes,
|
UploadTarget.Nodes,
|
||||||
values.cover ? [values.cover] : []
|
values.cover ? [values.cover] : [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const background = values.cover ? getURL(values.cover, ImagePresets['300']) : null;
|
const background = values.cover
|
||||||
|
? getURL(values.cover, imagePresets['300'])
|
||||||
|
: null;
|
||||||
const preview = pendingImages?.[0]?.thumbnail || '';
|
const preview = pendingImages?.[0]?.thumbnail || '';
|
||||||
|
|
||||||
const onDropCover = useCallback(() => {
|
const onDropCover = useCallback(() => {
|
||||||
|
@ -31,13 +33,13 @@ const EditorUploadCoverButton: FC<IProps> = () => {
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(event.target.files || [])
|
const files = Array.from(event.target.files || [])
|
||||||
.filter(file => getFileType(file) === UploadType.Image)
|
.filter((file) => getFileType(file) === UploadType.Image)
|
||||||
.slice(0, 1);
|
.slice(0, 1);
|
||||||
|
|
||||||
const result = await uploadFile(files[0]);
|
const result = await uploadFile(files[0]);
|
||||||
setFieldValue('cover', result);
|
setFieldValue('cover', result);
|
||||||
},
|
},
|
||||||
[uploadFile, setFieldValue]
|
[uploadFile, setFieldValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SwiperClass from 'swiper/types/swiper-class';
|
||||||
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
import { ImagePresets, URLS } from '~/constants/urls';
|
import { imagePresets, URLS } from '~/constants/urls';
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
import { useNavigation } from '~/hooks/navigation/useNavigation';
|
import { useNavigation } from '~/hooks/navigation/useNavigation';
|
||||||
import { IFlowNode } from '~/types';
|
import { IFlowNode } from '~/types';
|
||||||
|
@ -45,7 +45,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
||||||
const preset = useMemo(
|
const preset = useMemo(
|
||||||
() => (isTablet ? ImagePresets.cover : ImagePresets.small_hero),
|
() => (isTablet ? imagePresets.cover : imagePresets.small_hero),
|
||||||
[isTablet],
|
[isTablet],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { FC } from 'react';
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const UserButton: FC<IProps> = ({ username, photo, onClick }) => {
|
||||||
<button className={styles.wrap} onClick={onClick}>
|
<button className={styles.wrap} onClick={onClick}>
|
||||||
<Group horizontal className={styles.user_button}>
|
<Group horizontal className={styles.user_button}>
|
||||||
<div className={styles.username}>{username}</div>
|
<div className={styles.username}>{username}</div>
|
||||||
<Avatar url={getURL(photo, ImagePresets.avatar)} size={32} />
|
<Avatar url={getURL(photo, imagePresets.avatar)} size={32} />
|
||||||
</Group>
|
</Group>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import React, { FC, MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
FC,
|
||||||
|
MouseEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -6,7 +12,7 @@ import { ImageWithSSRLoad } from '~/components/common/ImageWithSSRLoad';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useResizeHandler } from '~/hooks/dom/useResizeHandler';
|
import { useResizeHandler } from '~/hooks/dom/useResizeHandler';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
@ -24,7 +30,13 @@ interface IProps {
|
||||||
const DEFAULT_WIDTH = 1920;
|
const DEFAULT_WIDTH = 1920;
|
||||||
const DEFAULT_HEIGHT = 1020;
|
const DEFAULT_HEIGHT = 1020;
|
||||||
|
|
||||||
const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className }) => {
|
const ImagePreloader: FC<IProps> = ({
|
||||||
|
file,
|
||||||
|
color,
|
||||||
|
onLoad,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
const [maxHeight, setMaxHeight] = useState(0);
|
const [maxHeight, setMaxHeight] = useState(0);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
@ -47,8 +59,11 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
||||||
}, [setHasError]);
|
}, [setHasError]);
|
||||||
|
|
||||||
const [width, height] = useMemo(
|
const [width, height] = useMemo(
|
||||||
() => [file?.metadata?.width || DEFAULT_WIDTH, file?.metadata?.height || DEFAULT_HEIGHT],
|
() => [
|
||||||
[file]
|
file?.metadata?.width || DEFAULT_WIDTH,
|
||||||
|
file?.metadata?.height || DEFAULT_HEIGHT,
|
||||||
|
],
|
||||||
|
[file],
|
||||||
);
|
);
|
||||||
|
|
||||||
useResizeHandler(onResize);
|
useResizeHandler(onResize);
|
||||||
|
@ -74,11 +89,18 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<g filter="url(#f1)">
|
<g filter="url(#f1)">
|
||||||
<rect fill={fill} width="100%" height="100%" stroke="none" rx="8" ry="8" />
|
<rect
|
||||||
|
fill={fill}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
stroke="none"
|
||||||
|
rx="8"
|
||||||
|
ry="8"
|
||||||
|
/>
|
||||||
|
|
||||||
{!hasError && (
|
{!hasError && (
|
||||||
<image
|
<image
|
||||||
xlinkHref={getURL(file, ImagePresets['300'])}
|
xlinkHref={getURL(file, imagePresets['300'])}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
|
@ -88,8 +110,12 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<ImageWithSSRLoad
|
<ImageWithSSRLoad
|
||||||
className={classNames(styles.image, { [styles.is_loaded]: loaded }, className)}
|
className={classNames(
|
||||||
src={getURL(file, ImagePresets['1600'])}
|
styles.image,
|
||||||
|
{ [styles.is_loaded]: loaded },
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
src={getURL(file, imagePresets['1600'])}
|
||||||
alt=""
|
alt=""
|
||||||
key={file.id}
|
key={file.id}
|
||||||
onLoad={onImageLoad}
|
onLoad={onImageLoad}
|
||||||
|
@ -98,7 +124,9 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!loaded && !hasError && <LoaderCircle className={styles.icon} size={64} />}
|
{!loaded && !hasError && (
|
||||||
|
<LoaderCircle className={styles.icon} size={64} />
|
||||||
|
)}
|
||||||
|
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { INodeComponentProps } from '~/constants/node';
|
import { INodeComponentProps } from '~/constants/node';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { path } from '~/utils/ramda';
|
import { path } from '~/utils/ramda';
|
||||||
|
@ -19,7 +19,12 @@ const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div
|
<div
|
||||||
className={styles.slide}
|
className={styles.slide}
|
||||||
style={{ backgroundImage: `url("${getURL(path([0], images), ImagePresets.small_hero)}")` }}
|
style={{
|
||||||
|
backgroundImage: `url("${getURL(
|
||||||
|
path([0], images),
|
||||||
|
imagePresets.small_hero,
|
||||||
|
)}")`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import Image from 'next/future/image';
|
import Image from 'next/future/image';
|
||||||
import SwiperCore, {
|
import SwiperCore, {
|
||||||
|
@ -7,22 +8,26 @@ import SwiperCore, {
|
||||||
Navigation,
|
Navigation,
|
||||||
Pagination,
|
Pagination,
|
||||||
SwiperOptions,
|
SwiperOptions,
|
||||||
|
Lazy,
|
||||||
} from 'swiper';
|
} from 'swiper';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import SwiperClass from 'swiper/types/swiper-class';
|
import SwiperClass from 'swiper/types/swiper-class';
|
||||||
|
|
||||||
import { ImagePreloader } from '~/components/media/ImagePreloader';
|
import { ImagePreloader } from '~/components/media/ImagePreloader';
|
||||||
import { INodeComponentProps } from '~/constants/node';
|
import { INodeComponentProps } from '~/constants/node';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useModal } from '~/hooks/modal/useModal';
|
import { useModal } from '~/hooks/modal/useModal';
|
||||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
||||||
|
import { IFile } from '~/types';
|
||||||
import { normalizeBrightColor } from '~/utils/color';
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
import { imageSrcSets, ImagePreset } from '../../../constants/urls';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
SwiperCore.use([Navigation, Pagination, Keyboard]);
|
SwiperCore.use([Navigation, Pagination, Keyboard, Lazy]);
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
|
@ -34,6 +39,22 @@ const breakpoints: SwiperOptions['breakpoints'] = {
|
||||||
|
|
||||||
const pagination = { type: 'fraction' as const };
|
const pagination = { type: 'fraction' as const };
|
||||||
|
|
||||||
|
const lazy = {
|
||||||
|
enabled: true,
|
||||||
|
loadPrevNextAmount: 1,
|
||||||
|
loadOnTransitionStart: true,
|
||||||
|
loadPrevNext: true,
|
||||||
|
checkInView: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSrcSet = (file: IFile) =>
|
||||||
|
Object.keys(imageSrcSets)
|
||||||
|
.map(
|
||||||
|
(preset) =>
|
||||||
|
`${getURL(file, preset as ImagePreset)} ${imageSrcSets[preset]}w`,
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
const NodeImageSwiperBlock: FC<IProps> = observer(({ node }) => {
|
const NodeImageSwiperBlock: FC<IProps> = observer(({ node }) => {
|
||||||
const [controlledSwiper, setControlledSwiper] = useState<
|
const [controlledSwiper, setControlledSwiper] = useState<
|
||||||
SwiperClass | undefined
|
SwiperClass | undefined
|
||||||
|
@ -126,20 +147,22 @@ const NodeImageSwiperBlock: FC<IProps> = observer(({ node }) => {
|
||||||
autoHeight
|
autoHeight
|
||||||
zoom
|
zoom
|
||||||
navigation
|
navigation
|
||||||
|
watchSlidesProgress
|
||||||
|
lazy={lazy}
|
||||||
>
|
>
|
||||||
{images.map((file, i) => (
|
{images.map((file, i) => (
|
||||||
<SwiperSlide className={styles.slide} key={file.id}>
|
<SwiperSlide className={styles.slide} key={file.id}>
|
||||||
<Image
|
<img
|
||||||
style={{ backgroundColor: file.metadata?.dominant_color }}
|
style={{ backgroundColor: file.metadata?.dominant_color }}
|
||||||
src={getURL(file, ImagePresets['1600'])}
|
data-srcset={generateSrcSet(file)}
|
||||||
width={file.metadata?.width}
|
width={file.metadata?.width}
|
||||||
height={file.metadata?.height}
|
height={file.metadata?.height}
|
||||||
onLoad={updateSwiper}
|
onLoad={updateSwiper}
|
||||||
onClick={() => onOpenPhotoSwipe(i)}
|
onClick={() => onOpenPhotoSwipe(i)}
|
||||||
className={styles.image}
|
className={classNames(styles.image, 'swiper-lazy')}
|
||||||
color={normalizeBrightColor(file?.metadata?.dominant_color)}
|
color={normalizeBrightColor(file?.metadata?.dominant_color)}
|
||||||
alt=""
|
alt=""
|
||||||
priority={i < 2}
|
sizes="(max-width: 560px) 100vw, 50vh"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||||
import { ImageWithSSRLoad } from '~/components/common/ImageWithSSRLoad';
|
import { ImageWithSSRLoad } from '~/components/common/ImageWithSSRLoad';
|
||||||
import { Square } from '~/components/common/Square';
|
import { Square } from '~/components/common/Square';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||||
import { useGotoNode } from '~/hooks/node/useGotoNode';
|
import { useGotoNode } from '~/hooks/node/useGotoNode';
|
||||||
import { INode } from '~/types';
|
import { INode } from '~/types';
|
||||||
|
@ -42,7 +42,7 @@ const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
||||||
const thumb = useMemo(
|
const thumb = useMemo(
|
||||||
() =>
|
() =>
|
||||||
item.thumbnail
|
item.thumbnail
|
||||||
? getURL({ url: item.thumbnail }, ImagePresets.avatar)
|
? getURL({ url: item.thumbnail }, imagePresets.avatar)
|
||||||
: '',
|
: '',
|
||||||
[item],
|
[item],
|
||||||
);
|
);
|
||||||
|
@ -68,7 +68,7 @@ const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
const image = useMemo(
|
const image = useMemo(
|
||||||
() => getURL({ url: item.thumbnail }, ImagePresets.avatar),
|
() => getURL({ url: item.thumbnail }, imagePresets.avatar),
|
||||||
[item.thumbnail],
|
[item.thumbnail],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { ChangeEvent, FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
import { ImageUpload } from '~/components/upload/ImageUpload';
|
import { ImageUpload } from '~/components/upload/ImageUpload';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
@ -18,8 +18,18 @@ interface SortableImageGridProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
const renderItem = ({ item, onDelete }: { item: IFile; onDelete: (fileId: number) => void }) => (
|
const renderItem = ({
|
||||||
<ImageUpload id={item.id} thumb={getURL(item, ImagePresets.cover)} onDrop={onDelete} />
|
item,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
item: IFile;
|
||||||
|
onDelete: (fileId: number) => void;
|
||||||
|
}) => (
|
||||||
|
<ImageUpload
|
||||||
|
id={item.id}
|
||||||
|
thumb={getURL(item, imagePresets.cover)}
|
||||||
|
onDrop={onDelete}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderLocked = ({
|
const renderLocked = ({
|
||||||
|
@ -29,7 +39,12 @@ const renderLocked = ({
|
||||||
locked: UploadStatus;
|
locked: UploadStatus;
|
||||||
onDelete: (fileId: number) => void;
|
onDelete: (fileId: number) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<ImageUpload thumb={locked.thumbnail} onDrop={onDelete} progress={locked.progress} uploading />
|
<ImageUpload
|
||||||
|
thumb={locked.thumbnail}
|
||||||
|
onDrop={onDelete}
|
||||||
|
progress={locked.progress}
|
||||||
|
uploading
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SortableImageGrid: FC<SortableImageGridProps> = ({
|
const SortableImageGrid: FC<SortableImageGridProps> = ({
|
||||||
|
@ -46,8 +61,8 @@ const SortableImageGrid: FC<SortableImageGridProps> = ({
|
||||||
<SortableGrid
|
<SortableGrid
|
||||||
items={items}
|
items={items}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
getID={it => it.id}
|
getID={(it) => it.id}
|
||||||
getLockedID={it => it.id}
|
getLockedID={(it) => it.id}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
renderItemProps={props}
|
renderItemProps={props}
|
||||||
renderLocked={renderLocked}
|
renderLocked={renderLocked}
|
||||||
|
|
|
@ -1,49 +1,61 @@
|
||||||
import { FlowDisplayVariant, INode } from "~/types";
|
import { FlowDisplayVariant, INode } from '~/types';
|
||||||
|
|
||||||
export const URLS = {
|
export const URLS = {
|
||||||
BASE: "/",
|
BASE: '/',
|
||||||
LAB: "/lab",
|
LAB: '/lab',
|
||||||
BORIS: "/boris",
|
BORIS: '/boris',
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: "/auth/login",
|
LOGIN: '/auth/login',
|
||||||
},
|
},
|
||||||
EXAMPLES: {
|
EXAMPLES: {
|
||||||
EDITOR: "/examples/edit",
|
EDITOR: '/examples/edit',
|
||||||
IMAGE: "/examples/image",
|
IMAGE: '/examples/image',
|
||||||
},
|
},
|
||||||
ERRORS: {
|
ERRORS: {
|
||||||
NOT_FOUND: "/lost",
|
NOT_FOUND: '/lost',
|
||||||
BACKEND_DOWN: "/oopsie",
|
BACKEND_DOWN: '/oopsie',
|
||||||
},
|
},
|
||||||
NODE_URL: (id: INode["id"] | string) => `/post${id}`,
|
NODE_URL: (id: INode['id'] | string) => `/post${id}`,
|
||||||
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
||||||
SETTINGS: {
|
SETTINGS: {
|
||||||
BASE: "/settings",
|
BASE: '/settings',
|
||||||
NOTES: "/settings/notes",
|
NOTES: '/settings/notes',
|
||||||
TRASH: "/settings/trash",
|
TRASH: '/settings/trash',
|
||||||
},
|
},
|
||||||
NOTES: "/notes/",
|
NOTES: '/notes/',
|
||||||
NOTE: (id: number) => `/notes/${id}`,
|
NOTE: (id: number) => `/notes/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImagePresets = {
|
export const imagePresets = {
|
||||||
"1600": "1600",
|
'1600': '1600',
|
||||||
"600": "600",
|
'900': '900',
|
||||||
"300": "300",
|
'1200': '1200',
|
||||||
cover: "cover",
|
'600': '600',
|
||||||
small_hero: "small_hero",
|
'300': '300',
|
||||||
avatar: "avatar",
|
cover: 'cover',
|
||||||
flow_square: "flow_square",
|
small_hero: 'small_hero',
|
||||||
flow_vertical: "flow_vertical",
|
avatar: 'avatar',
|
||||||
flow_horizontal: "flow_horizontal",
|
flow_square: 'flow_square',
|
||||||
|
flow_vertical: 'flow_vertical',
|
||||||
|
flow_horizontal: 'flow_horizontal',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type ImagePreset = typeof imagePresets[keyof typeof imagePresets];
|
||||||
|
|
||||||
|
export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
|
||||||
|
[imagePresets[1600]]: 1600,
|
||||||
|
[imagePresets[900]]: 900,
|
||||||
|
[imagePresets[1200]]: 1200,
|
||||||
|
[imagePresets[600]]: 600,
|
||||||
|
[imagePresets[300]]: 300,
|
||||||
|
};
|
||||||
|
|
||||||
export const flowDisplayToPreset: Record<
|
export const flowDisplayToPreset: Record<
|
||||||
FlowDisplayVariant,
|
FlowDisplayVariant,
|
||||||
typeof ImagePresets[keyof typeof ImagePresets]
|
typeof imagePresets[keyof typeof imagePresets]
|
||||||
> = {
|
> = {
|
||||||
single: "flow_square",
|
single: 'flow_square',
|
||||||
quadro: "flow_square",
|
quadro: 'flow_square',
|
||||||
vertical: "flow_vertical",
|
vertical: 'flow_vertical',
|
||||||
horizontal: "flow_horizontal",
|
horizontal: 'flow_horizontal',
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
|
||||||
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js';
|
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js';
|
||||||
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js';
|
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js';
|
||||||
|
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
import { useModal } from '~/hooks/modal/useModal';
|
import { useModal } from '~/hooks/modal/useModal';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
|
@ -25,18 +25,18 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
const { isTablet } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new Promise(async resolve => {
|
new Promise(async (resolve) => {
|
||||||
const images = await Promise.all(
|
const images = await Promise.all(
|
||||||
items.map(
|
items.map(
|
||||||
image =>
|
(image) =>
|
||||||
new Promise(resolveImage => {
|
new Promise((resolveImage) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
resolveImage({
|
resolveImage({
|
||||||
src: getURL(
|
src: getURL(
|
||||||
image,
|
image,
|
||||||
isTablet ? ImagePresets[900] : ImagePresets[1600],
|
isTablet ? imagePresets[900] : imagePresets[1600],
|
||||||
),
|
),
|
||||||
h: img.naturalHeight,
|
h: img.naturalHeight,
|
||||||
w: img.naturalWidth,
|
w: img.naturalWidth,
|
||||||
|
@ -47,13 +47,13 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
resolveImage({});
|
resolveImage({});
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = getURL(image, ImagePresets[1600]);
|
img.src = getURL(image, imagePresets[1600]);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve(images);
|
resolve(images);
|
||||||
}).then(images => {
|
}).then((images) => {
|
||||||
const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, {
|
const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, {
|
||||||
index: index || 0,
|
index: index || 0,
|
||||||
closeOnScroll: false,
|
closeOnScroll: false,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { FC } from 'react';
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { Markdown } from '~/components/containers/Markdown';
|
import { Markdown } from '~/components/containers/Markdown';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IUser } from '~/types/auth';
|
import { IUser } from '~/types/auth';
|
||||||
import { formatText } from '~/utils/dom';
|
import { formatText } from '~/utils/dom';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile, isLoading }) => {
|
||||||
username={username}
|
username={username}
|
||||||
url={profile?.photo?.url}
|
url={profile?.photo?.url}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
preset={ImagePresets['600']}
|
preset={imagePresets['600']}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.region}>
|
<div className={styles.region}>
|
||||||
|
|
|
@ -5,8 +5,12 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||||
import isAfter from 'date-fns/isAfter';
|
import isAfter from 'date-fns/isAfter';
|
||||||
import ru from 'date-fns/locale/ru';
|
import ru from 'date-fns/locale/ru';
|
||||||
|
|
||||||
import { COMMENT_BLOCK_DETECTORS, COMMENT_BLOCK_TYPES, ICommentBlock } from '~/constants/comment';
|
import {
|
||||||
import { ImagePresets } from '~/constants/urls';
|
COMMENT_BLOCK_DETECTORS,
|
||||||
|
COMMENT_BLOCK_TYPES,
|
||||||
|
ICommentBlock,
|
||||||
|
} from '~/constants/comment';
|
||||||
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IFile, ValueOf } from '~/types';
|
import { IFile, ValueOf } from '~/types';
|
||||||
import { CONFIG } from '~/utils/config';
|
import { CONFIG } from '~/utils/config';
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +26,8 @@ import {
|
||||||
import { pipe } from '~/utils/ramda';
|
import { pipe } from '~/utils/ramda';
|
||||||
import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText';
|
import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText';
|
||||||
|
|
||||||
|
import { ImagePreset } from '../constants/urls';
|
||||||
|
|
||||||
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
||||||
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
||||||
|
|
||||||
|
@ -36,7 +42,7 @@ export const describeArc = (
|
||||||
y: number,
|
y: number,
|
||||||
radius: number,
|
radius: number,
|
||||||
startAngle: number = 0,
|
startAngle: number = 0,
|
||||||
endAngle: number = 360
|
endAngle: number = 360,
|
||||||
): string => {
|
): string => {
|
||||||
const start = polarToCartesian(x, y, radius, endAngle);
|
const start = polarToCartesian(x, y, radius, endAngle);
|
||||||
const end = polarToCartesian(x, y, radius, startAngle);
|
const end = polarToCartesian(x, y, radius, startAngle);
|
||||||
|
@ -64,12 +70,12 @@ export const describeArc = (
|
||||||
].join(' ');
|
].join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getURLFromString = (
|
export const getURLFromString = (url?: string, size?: ImagePreset): string => {
|
||||||
url?: string,
|
|
||||||
size?: typeof ImagePresets[keyof typeof ImagePresets]
|
|
||||||
): string => {
|
|
||||||
if (size) {
|
if (size) {
|
||||||
return (url || '').replace('REMOTE_CURRENT://', `${CONFIG.remoteCurrent}cache/${size}/`);
|
return (url || '').replace(
|
||||||
|
'REMOTE_CURRENT://',
|
||||||
|
`${CONFIG.remoteCurrent}cache/${size}/`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (url || '').replace('REMOTE_CURRENT://', CONFIG.remoteCurrent);
|
return (url || '').replace('REMOTE_CURRENT://', CONFIG.remoteCurrent);
|
||||||
|
@ -77,7 +83,7 @@ export const getURLFromString = (
|
||||||
|
|
||||||
export const getURL = (
|
export const getURL = (
|
||||||
file: Partial<IFile> | undefined,
|
file: Partial<IFile> | undefined,
|
||||||
size?: typeof ImagePresets[keyof typeof ImagePresets]
|
size?: ImagePreset,
|
||||||
) => {
|
) => {
|
||||||
return file?.url ? getURLFromString(file.url, size) : '';
|
return file?.url ? getURLFromString(file.url, size) : '';
|
||||||
};
|
};
|
||||||
|
@ -90,27 +96,34 @@ export const formatText = pipe(
|
||||||
formatTextDash,
|
formatTextDash,
|
||||||
formatTextMarkdown,
|
formatTextMarkdown,
|
||||||
formatTextSanitizeTags,
|
formatTextSanitizeTags,
|
||||||
formatTextClickableUsernames
|
formatTextClickableUsernames,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatTextParagraphs = (text: string): string => (text && formatText(text)) || '';
|
export const formatTextParagraphs = (text: string): string =>
|
||||||
|
(text && formatText(text)) || '';
|
||||||
|
|
||||||
export const findBlockType = (line: string): ValueOf<typeof COMMENT_BLOCK_TYPES> => {
|
export const findBlockType = (
|
||||||
const match = Object.values(COMMENT_BLOCK_DETECTORS).find(detector => line.match(detector.test));
|
line: string,
|
||||||
|
): ValueOf<typeof COMMENT_BLOCK_TYPES> => {
|
||||||
|
const match = Object.values(COMMENT_BLOCK_DETECTORS).find((detector) =>
|
||||||
|
line.match(detector.test),
|
||||||
|
);
|
||||||
return (match && match.type) || COMMENT_BLOCK_TYPES.TEXT;
|
return (match && match.type) || COMMENT_BLOCK_TYPES.TEXT;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const splitCommentByBlocks = (text: string): ICommentBlock[] =>
|
export const splitCommentByBlocks = (text: string): ICommentBlock[] =>
|
||||||
pipe(
|
pipe(
|
||||||
splitTextByYoutube,
|
splitTextByYoutube,
|
||||||
splitTextOmitEmpty
|
splitTextOmitEmpty,
|
||||||
)([text]).map(line => ({
|
)([text]).map((line) => ({
|
||||||
type: findBlockType(line),
|
type: findBlockType(line),
|
||||||
content: line,
|
content: line,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const formatCommentText = (author?: string, text?: string): ICommentBlock[] =>
|
export const formatCommentText = (
|
||||||
author && text ? splitCommentByBlocks(text) : [];
|
author?: string,
|
||||||
|
text?: string,
|
||||||
|
): ICommentBlock[] => (author && text ? splitCommentByBlocks(text) : []);
|
||||||
|
|
||||||
export const getPrettyDate = (date?: string): string => {
|
export const getPrettyDate = (date?: string): string => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
|
@ -135,10 +148,12 @@ export const getYoutubeThumb = (url: string) => {
|
||||||
const match =
|
const match =
|
||||||
url &&
|
url &&
|
||||||
url.match(
|
url.match(
|
||||||
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?[\w?=]*)?/
|
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?[\w?=]*)?/,
|
||||||
);
|
);
|
||||||
|
|
||||||
return match && match[1] ? `https://i.ytimg.com/vi/${match[1]}/hqdefault.jpg` : null;
|
return match && match[1]
|
||||||
|
? `https://i.ytimg.com/vi/${match[1]}/hqdefault.jpg`
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stringToColour = (str: string) => {
|
export const stringToColour = (str: string) => {
|
||||||
|
@ -191,5 +206,7 @@ export const sizeOf = (bytes: number): string => {
|
||||||
return '0.00 B';
|
return '0.00 B';
|
||||||
}
|
}
|
||||||
let e = Math.floor(Math.log(bytes) / Math.log(1024));
|
let e = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B';
|
return (
|
||||||
|
(bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue