mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46: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 { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IComment, IFile } from '~/types';
|
||||
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
||||
import { append, assocPath, path, reduce } from '~/utils/ramda';
|
||||
|
@ -141,7 +141,7 @@ const CommentContent: FC<IProps> = memo(
|
|||
onClick={() => onShowImageModal(groupped.image, index)}
|
||||
>
|
||||
<img
|
||||
src={getURL(file, ImagePresets['600'])}
|
||||
src={getURL(file, imagePresets['600'])}
|
||||
alt={file.name}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { forwardRef } from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { Square } from '~/components/common/Square';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||
import { getURLFromString } from '~/utils/dom';
|
||||
import { DivProps } from '~/utils/types';
|
||||
|
@ -14,12 +14,12 @@ interface Props extends DivProps {
|
|||
url?: string;
|
||||
username?: string;
|
||||
size?: number;
|
||||
preset?: typeof ImagePresets[keyof typeof ImagePresets];
|
||||
preset?: typeof imagePresets[keyof typeof imagePresets];
|
||||
}
|
||||
|
||||
const Avatar = forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{ url, username, size, className, preset = ImagePresets.avatar, ...rest },
|
||||
{ url, username, size, className, preset = imagePresets.avatar, ...rest },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IUser } from '~/types/auth';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
|
@ -19,14 +19,14 @@ const CoverBackdrop: FC<IProps> = ({ cover }) => {
|
|||
|
||||
const onLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]);
|
||||
|
||||
const image = getURL(cover, ImagePresets.cover);
|
||||
const image = getURL(cover, imagePresets.cover);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cover || !cover.url || !ref || !ref.current) return;
|
||||
|
||||
ref.current.src = '';
|
||||
setIsLoaded(false);
|
||||
ref.current.src = getURL(cover, ImagePresets.cover);
|
||||
ref.current.src = getURL(cover, imagePresets.cover);
|
||||
}, [cover]);
|
||||
|
||||
if (!cover) return null;
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { createContext, FC, useContext, useState } from 'react';
|
|||
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
|
@ -27,9 +27,11 @@ const PageCoverProvider: FC = ({ children }) => {
|
|||
createPortal(
|
||||
<div
|
||||
className={styles.wrap}
|
||||
style={{ backgroundImage: `url("${getURL(cover, ImagePresets.cover)}")` }}
|
||||
style={{
|
||||
backgroundImage: `url("${getURL(cover, imagePresets.cover)}")`,
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { ChangeEvent, FC, useCallback, useEffect } from 'react';
|
|||
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { UploadSubject, UploadTarget, UploadType } from '~/constants/uploads';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useUploader } from '~/hooks/data/useUploader';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
@ -18,10 +18,12 @@ const EditorUploadCoverButton: FC<IProps> = () => {
|
|||
const { uploadFile, files, pendingImages } = useUploader(
|
||||
UploadSubject.Editor,
|
||||
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 onDropCover = useCallback(() => {
|
||||
|
@ -31,13 +33,13 @@ const EditorUploadCoverButton: FC<IProps> = () => {
|
|||
const onInputChange = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || [])
|
||||
.filter(file => getFileType(file) === UploadType.Image)
|
||||
.filter((file) => getFileType(file) === UploadType.Image)
|
||||
.slice(0, 1);
|
||||
|
||||
const result = await uploadFile(files[0]);
|
||||
setFieldValue('cover', result);
|
||||
},
|
||||
[uploadFile, setFieldValue]
|
||||
[uploadFile, setFieldValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import SwiperClass from 'swiper/types/swiper-class';
|
|||
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
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 { useNavigation } from '~/hooks/navigation/useNavigation';
|
||||
import { IFlowNode } from '~/types';
|
||||
|
@ -45,7 +45,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
|||
>(undefined);
|
||||
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
||||
const preset = useMemo(
|
||||
() => (isTablet ? ImagePresets.cover : ImagePresets.small_hero),
|
||||
() => (isTablet ? imagePresets.cover : imagePresets.small_hero),
|
||||
[isTablet],
|
||||
);
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FC } from 'react';
|
|||
import { Avatar } from '~/components/common/Avatar';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
|
@ -20,7 +20,7 @@ const UserButton: FC<IProps> = ({ username, photo, onClick }) => {
|
|||
<button className={styles.wrap} onClick={onClick}>
|
||||
<Group horizontal className={styles.user_button}>
|
||||
<div className={styles.username}>{username}</div>
|
||||
<Avatar url={getURL(photo, ImagePresets.avatar)} size={32} />
|
||||
<Avatar url={getURL(photo, imagePresets.avatar)} size={32} />
|
||||
</Group>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +12,7 @@ import { ImageWithSSRLoad } from '~/components/common/ImageWithSSRLoad';
|
|||
import { Icon } from '~/components/input/Icon';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useResizeHandler } from '~/hooks/dom/useResizeHandler';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
@ -24,7 +30,13 @@ interface IProps {
|
|||
const DEFAULT_WIDTH = 1920;
|
||||
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 [loaded, setLoaded] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
@ -47,8 +59,11 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
|||
}, [setHasError]);
|
||||
|
||||
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);
|
||||
|
@ -74,11 +89,18 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
|||
</defs>
|
||||
|
||||
<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 && (
|
||||
<image
|
||||
xlinkHref={getURL(file, ImagePresets['300'])}
|
||||
xlinkHref={getURL(file, imagePresets['300'])}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onLoad={onLoad}
|
||||
|
@ -88,8 +110,12 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
|||
</svg>
|
||||
|
||||
<ImageWithSSRLoad
|
||||
className={classNames(styles.image, { [styles.is_loaded]: loaded }, className)}
|
||||
src={getURL(file, ImagePresets['1600'])}
|
||||
className={classNames(
|
||||
styles.image,
|
||||
{ [styles.is_loaded]: loaded },
|
||||
className,
|
||||
)}
|
||||
src={getURL(file, imagePresets['1600'])}
|
||||
alt=""
|
||||
key={file.id}
|
||||
onLoad={onImageLoad}
|
||||
|
@ -98,7 +124,9 @@ const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className })
|
|||
onError={onError}
|
||||
/>
|
||||
|
||||
{!loaded && !hasError && <LoaderCircle className={styles.icon} size={64} />}
|
||||
{!loaded && !hasError && (
|
||||
<LoaderCircle className={styles.icon} size={64} />
|
||||
)}
|
||||
|
||||
{hasError && (
|
||||
<div className={styles.error}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { INodeComponentProps } from '~/constants/node';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
||||
import { getURL } from '~/utils/dom';
|
||||
import { path } from '~/utils/ramda';
|
||||
|
@ -19,7 +19,12 @@ const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
|||
<div className={styles.wrap}>
|
||||
<div
|
||||
className={styles.slide}
|
||||
style={{ backgroundImage: `url("${getURL(path([0], images), ImagePresets.small_hero)}")` }}
|
||||
style={{
|
||||
backgroundImage: `url("${getURL(
|
||||
path([0], images),
|
||||
imagePresets.small_hero,
|
||||
)}")`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import Image from 'next/future/image';
|
||||
import SwiperCore, {
|
||||
|
@ -7,22 +8,26 @@ import SwiperCore, {
|
|||
Navigation,
|
||||
Pagination,
|
||||
SwiperOptions,
|
||||
Lazy,
|
||||
} from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import SwiperClass from 'swiper/types/swiper-class';
|
||||
|
||||
import { ImagePreloader } from '~/components/media/ImagePreloader';
|
||||
import { INodeComponentProps } from '~/constants/node';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
||||
import { IFile } from '~/types';
|
||||
import { normalizeBrightColor } from '~/utils/color';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
import { imageSrcSets, ImagePreset } from '../../../constants/urls';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
SwiperCore.use([Navigation, Pagination, Keyboard]);
|
||||
SwiperCore.use([Navigation, Pagination, Keyboard, Lazy]);
|
||||
|
||||
interface IProps extends INodeComponentProps {}
|
||||
|
||||
|
@ -34,6 +39,22 @@ const breakpoints: SwiperOptions['breakpoints'] = {
|
|||
|
||||
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 [controlledSwiper, setControlledSwiper] = useState<
|
||||
SwiperClass | undefined
|
||||
|
@ -126,20 +147,22 @@ const NodeImageSwiperBlock: FC<IProps> = observer(({ node }) => {
|
|||
autoHeight
|
||||
zoom
|
||||
navigation
|
||||
watchSlidesProgress
|
||||
lazy={lazy}
|
||||
>
|
||||
{images.map((file, i) => (
|
||||
<SwiperSlide className={styles.slide} key={file.id}>
|
||||
<Image
|
||||
<img
|
||||
style={{ backgroundColor: file.metadata?.dominant_color }}
|
||||
src={getURL(file, ImagePresets['1600'])}
|
||||
data-srcset={generateSrcSet(file)}
|
||||
width={file.metadata?.width}
|
||||
height={file.metadata?.height}
|
||||
onLoad={updateSwiper}
|
||||
onClick={() => onOpenPhotoSwipe(i)}
|
||||
className={styles.image}
|
||||
className={classNames(styles.image, 'swiper-lazy')}
|
||||
color={normalizeBrightColor(file?.metadata?.dominant_color)}
|
||||
alt=""
|
||||
priority={i < 2}
|
||||
sizes="(max-width: 560px) 100vw, 50vh"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
|
|
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
|||
import { ImageWithSSRLoad } from '~/components/common/ImageWithSSRLoad';
|
||||
import { Square } from '~/components/common/Square';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||
import { useGotoNode } from '~/hooks/node/useGotoNode';
|
||||
import { INode } from '~/types';
|
||||
|
@ -42,7 +42,7 @@ const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
|||
const thumb = useMemo(
|
||||
() =>
|
||||
item.thumbnail
|
||||
? getURL({ url: item.thumbnail }, ImagePresets.avatar)
|
||||
? getURL({ url: item.thumbnail }, imagePresets.avatar)
|
||||
: '',
|
||||
[item],
|
||||
);
|
||||
|
@ -68,7 +68,7 @@ const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
|||
}, [width]);
|
||||
|
||||
const image = useMemo(
|
||||
() => getURL({ url: item.thumbnail }, ImagePresets.avatar),
|
||||
() => getURL({ url: item.thumbnail }, imagePresets.avatar),
|
||||
[item.thumbnail],
|
||||
);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { ChangeEvent, FC, useCallback } from 'react';
|
|||
|
||||
import { Avatar } from '~/components/common/Avatar';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { ImageUpload } from '~/components/upload/ImageUpload';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
@ -18,8 +18,18 @@ interface SortableImageGridProps {
|
|||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
const renderItem = ({ item, onDelete }: { item: IFile; onDelete: (fileId: number) => void }) => (
|
||||
<ImageUpload id={item.id} thumb={getURL(item, ImagePresets.cover)} onDrop={onDelete} />
|
||||
const renderItem = ({
|
||||
item,
|
||||
onDelete,
|
||||
}: {
|
||||
item: IFile;
|
||||
onDelete: (fileId: number) => void;
|
||||
}) => (
|
||||
<ImageUpload
|
||||
id={item.id}
|
||||
thumb={getURL(item, imagePresets.cover)}
|
||||
onDrop={onDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderLocked = ({
|
||||
|
@ -29,7 +39,12 @@ const renderLocked = ({
|
|||
locked: UploadStatus;
|
||||
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> = ({
|
||||
|
@ -46,8 +61,8 @@ const SortableImageGrid: FC<SortableImageGridProps> = ({
|
|||
<SortableGrid
|
||||
items={items}
|
||||
locked={locked}
|
||||
getID={it => it.id}
|
||||
getLockedID={it => it.id}
|
||||
getID={(it) => it.id}
|
||||
getLockedID={(it) => it.id}
|
||||
renderItem={renderItem}
|
||||
renderItemProps={props}
|
||||
renderLocked={renderLocked}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue