1
0
Fork 0
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:
Fedor Katurov 2022-12-06 21:00:37 +06:00
parent 991f829216
commit 10bb6f01b5
17 changed files with 210 additions and 106 deletions

View file

@ -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>

View file

@ -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 (

View file

@ -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;

View file

@ -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}

View file

@ -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(() => {

View file

@ -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],
); );

View file

@ -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>
); );

View file

@ -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}>

View file

@ -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>
); );

View file

@ -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>
))} ))}

View file

@ -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],
); );

View file

@ -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';

View file

@ -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}

View file

@ -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',
}; };

View file

@ -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,

View file

@ -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}>

View file

@ -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'
);
}; };