1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

refactor editos

This commit is contained in:
Fedor Katurov 2023-11-20 22:21:08 +06:00
parent 03ddb1862c
commit 5e9c111e0f
149 changed files with 416 additions and 317 deletions

View file

@ -1,42 +0,0 @@
import { FC } from 'react';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { NodeEditorProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { AudioGrid } from '../AudioGrid';
import { ImageGrid } from '../ImageGrid';
import styles from './styles.module.scss';
const AudioEditor: FC<NodeEditorProps> = () => {
const {
filesImages,
filesAudios,
uploadFiles,
setImages,
setAudios,
pendingAudios,
pendingImages,
} = useUploaderContext()!;
return (
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid
files={filesImages}
setFiles={setImages}
locked={pendingImages}
/>
<AudioGrid
files={filesAudios}
setFiles={setAudios}
locked={pendingAudios}
/>
</div>
</UploadDropzone>
);
};
export { AudioEditor };

View file

@ -1,6 +0,0 @@
@import "src/styles/variables";
.wrap {
padding-bottom: $upload_button_height + $gap;
min-height: 200px;
}

View file

@ -1,52 +0,0 @@
import React, { FC, useCallback } from 'react';
import { SortableAudioGrid } from '~/components/sortable/SortableAudioGrid';
import { UploadStatus } from '~/store/uploader/UploaderStore';
import { IFile } from '~/types';
interface IProps {
files: IFile[];
setFiles: (val: IFile[]) => void;
locked: UploadStatus[];
}
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
const onMove = useCallback(
(newFiles: IFile[]) => {
setFiles(newFiles);
},
[setFiles],
);
const onDrop = useCallback(
(remove_id: IFile['id']) => {
setFiles(files.filter((file) => file && file.id !== remove_id));
},
[setFiles, files],
);
const onTitleChange = useCallback(
(changeId: IFile['id'], title: string) => {
setFiles(
files.map((file) =>
file && file.id === changeId
? { ...file, metadata: { ...file.metadata, title } }
: file,
),
);
},
[setFiles, files],
);
return (
<SortableAudioGrid
onDelete={onDrop}
onTitleChange={onTitleChange}
onSortEnd={onMove}
items={files}
locked={locked}
/>
);
};
export { AudioGrid };

View file

@ -1,24 +0,0 @@
import React, { createElement, FC } from 'react';
import { NODE_PANEL_COMPONENTS } from '~/constants/node';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { has } from '~/utils/ramda';
import styles from './styles.module.scss';
const EditorActionsPanel: FC = () => {
const { values } = useNodeFormContext();
if (!values.type || !has(values.type, NODE_PANEL_COMPONENTS)) {
return null;
}
return (
<div className={styles.panel}>
{NODE_PANEL_COMPONENTS[values.type] &&
NODE_PANEL_COMPONENTS[values.type].map((el, key) => createElement(el, { key }))}
</div>
);
};
export { EditorActionsPanel };

View file

@ -1,30 +0,0 @@
@import "src/styles/variables";
.panel {
height: 72px;
position: absolute;
left: 0;
width: 100%;
bottom: 100%;
box-sizing: border-box;
padding: $gap;
z-index: 10;
display: flex;
flex-direction: row;
pointer-events: none;
touch-action: none;
& > * {
margin: 0 $gap * 0.5;
pointer-events: all;
touch-action: auto;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}

View file

@ -1,18 +0,0 @@
import React, { FC } from 'react';
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
import { UploadType } from '~/constants/uploads';
import { IEditorComponentProps } from '~/types/node';
type IProps = IEditorComponentProps & {};
const EditorAudioUploadButton: FC<IProps> = () => (
<EditorUploadButton
accept="audio/*"
icon="audio"
type={UploadType.Audio}
label="Добавить аудио"
/>
);
export { EditorAudioUploadButton };

View file

@ -1,44 +0,0 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { EditorActionsPanel } from '~/components/editors/EditorActionsPanel';
import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
const EditorButtons: FC = () => {
const { values, handleChange, isSubmitting } = useNodeFormContext();
const { isTablet } = useWindowSize();
return (
<Padder style={{ position: 'relative' }}>
<EditorActionsPanel />
<Group horizontal>
<Filler>
<InputText
title="Название"
value={values.title}
handler={handleChange('title')}
autoFocus={!isTablet}
maxLength={256}
disabled={isSubmitting}
/>
</Filler>
<Button
title={isTablet ? undefined : 'Сохранить'}
iconRight="check"
color={values.is_promoted ? 'flow' : 'lab'}
disabled={isSubmitting}
type="submit"
/>
</Group>
</Padder>
);
};
export { EditorButtons };

View file

@ -1,37 +0,0 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';
interface IProps {
onApprove: () => void;
onDecline: () => void;
}
const EditorConfirmClose: FC<IProps> = ({ onApprove, onDecline }) => (
<div className={styles.wrap}>
<Group className={styles.content}>
<div className={styles.title}>Точно закрыть?</div>
<div className={styles.subtitle}>
Все изменения будут потеряны, воспоминания затёрты, очевидцы умрут, над миром воссияет
ядерный рассвет.
</div>
<div />
<Group horizontal>
<Button color="gray" type="button" onClick={onApprove} autoFocus>
Да
</Button>
<Button type="button" onClick={onDecline}>
О боже, нет!
</Button>
</Group>
</Group>
</div>
);
export { EditorConfirmClose };

View file

@ -1,42 +0,0 @@
@import 'src/styles/variables.scss';
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.wrap {
@include blur;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 21;
display: flex;
align-items: center;
justify-content: center;
animation: appear 0.25s forwards;
}
.title {
text-transform: uppercase;
font: $font_18_semibold;
}
.content {
flex: 0 0 auto;
align-items: center;
justify-content: center;
}
.subtitle {
font: $font_12_medium;
color: $gray_50;
max-width: 300px;
}

View file

@ -1,12 +0,0 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { IEditorComponentProps } from '~/types/node';
import styles from './styles.module.scss';
type IProps = IEditorComponentProps & {};
const EditorFiller: FC<IProps> = () => <Filler className={styles.filler} />;
export { EditorFiller };

View file

@ -1,4 +0,0 @@
.filler {
touch-action: none;
pointer-events: none;
}

View file

@ -1,18 +0,0 @@
import React, { FC } from 'react';
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
import { UploadType } from '~/constants/uploads';
import { IEditorComponentProps } from '~/types/node';
type IProps = IEditorComponentProps & {};
const EditorImageUploadButton: FC<IProps> = () => (
<EditorUploadButton
accept="image/*"
icon="image"
type={UploadType.Image}
label="Добавить фоточек"
/>
);
export { EditorImageUploadButton };

View file

@ -1,45 +0,0 @@
import React, { FC, useCallback } from 'react';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { IEditorComponentProps } from '~/types/node';
import styles from './styles.module.scss';
interface IProps extends IEditorComponentProps {}
const EditorPublicSwitch: FC<IProps> = () => {
const { values, setFieldValue } = useNodeFormContext();
const onChange = useCallback(
() => setFieldValue('is_promoted', !values.is_promoted),
[values.is_promoted, setFieldValue],
);
return (
<Button
color={values.is_promoted ? 'flow' : 'lab'}
type="button"
size="giant"
label={
values.is_promoted
? 'Доступно всем на главной странице'
: 'Видно только сотрудникам в лаборатории'
}
onClick={onChange}
className={styles.button}
round
>
{values.is_promoted ? (
<Icon icon="waves" size={24} />
) : (
<div className={styles.lab_wrapper}>
<Icon icon="lab" size={24} />
</div>
)}
</Button>
);
};
export { EditorPublicSwitch };

View file

@ -1,63 +0,0 @@
@import "src/styles/variables";
@keyframes move_1 {
0% {
transform: scale(0) translate(0, 0);
opacity: 0;
}
50% {
transform: scale(1) translate(5px, -5px);
opacity: 1;
}
100% {
transform: scale(1.2) translate(-5px, -10px);
opacity: 0;
}
}
@keyframes move_2 {
0% {
transform: scale(0) translate(0, 0);
opacity: 0;
}
50% {
transform: scale(1) translate(-5px, -5px);
opacity: 1;
}
100% {
transform: scale(1.6) translate(5px, -10px);
opacity: 0;
}
}
.button {
}
.lab_wrapper {
position: relative;
bottom: -2px;
.button:hover & {
&:before,&:after {
content: ' ';
position: absolute;
top: 1px;
left: 10px;
width: 2px;
height: 2px;
box-shadow: white 0 0 0 2px;
border-radius: 4px;
animation: move_1 0.5s infinite linear;
}
&:after {
animation: move_2 0.5s -0.25s infinite linear;
}
}
}

View file

@ -1,59 +0,0 @@
import React, { ChangeEvent, FC, useCallback } from 'react';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';
import { UploadType } from '~/constants/uploads';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { IEditorComponentProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { getFileType } from '~/utils/uploader';
import styles from './styles.module.scss';
type IProps = IEditorComponentProps & {
accept?: string;
icon?: string;
type?: UploadType;
label?: string;
};
const EditorUploadButton: FC<IProps> = ({
accept = 'image/*',
icon = 'plus',
type = UploadType.Image,
label,
}) => {
const { uploadFiles } = useUploaderContext()!;
const { values } = useNodeFormContext();
const onInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const files = Array.from(event.target.files || []).filter(
(file) => !type || getFileType(file) === type,
);
uploadFiles(files);
},
[type, uploadFiles],
);
const color = values.is_promoted ? 'flow' : 'lab';
return (
<Button
type="button"
round
size="giant"
className={styles.wrap}
label={label}
color={color}
>
<Icon icon={icon} size={24} />
<input type="file" onChange={onInputChange} accept={accept} multiple />
</Button>
);
};
export { EditorUploadButton };

View file

@ -1,27 +0,0 @@
@import "src/styles/variables";
.wrap {
position: relative;
input {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: 1;
cursor: pointer;
}
}
.icon {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -1,74 +0,0 @@
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 { useUploader } from '~/hooks/data/useUploader';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { IEditorComponentProps } from '~/types/node';
import { getURL } from '~/utils/dom';
import { getFileType } from '~/utils/uploader';
import styles from './styles.module.scss';
type IProps = IEditorComponentProps & {};
const EditorUploadCoverButton: FC<IProps> = () => {
const { values, setFieldValue } = useNodeFormContext();
const { uploadFile, files, pendingImages } = useUploader(
UploadSubject.Editor,
UploadTarget.Nodes,
values.cover ? [values.cover] : [],
);
const background = values.cover
? getURL(values.cover, imagePresets['300'])
: null;
const preview = pendingImages?.[0]?.thumbnail || '';
const onDropCover = useCallback(() => {
setFieldValue('cover', null);
}, [setFieldValue]);
const onInputChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || [])
.filter((file) => getFileType(file) === UploadType.Image)
.slice(0, 1);
const result = await uploadFile(files[0]);
setFieldValue('cover', result);
},
[uploadFile, setFieldValue],
);
useEffect(() => {
if (!files.length) {
return;
}
setFieldValue('cover', files[files.length - 1]);
}, [files, setFieldValue]);
return (
<div className={styles.wrap}>
<div
className={styles.preview}
style={{ backgroundImage: `url("${preview || background}")` }}
>
<div className={styles.input}>
{!values.cover && <span>ОБЛОЖКА</span>}
<input type="file" accept="image/*" onChange={onInputChange} />
</div>
{values.cover && (
<div className={styles.button} onClick={onDropCover}>
<Icon icon="close" />
</div>
)}
</div>
</div>
);
};
export { EditorUploadCoverButton };

View file

@ -1,83 +0,0 @@
@import 'src/styles/variables';
.wrap {
@include outer_shadow();
height: $upload_button_height;
border-radius: ($upload_button_height * 0.5) !important;
position: relative;
border-radius: $radius;
cursor: pointer;
transition: opacity 0.5s;
background: $content_bg_lighter;
flex: 0 1 $upload_button_height * 4;
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 1;
}
input {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: 2;
cursor: pointer;
}
}
.input {
position: relative;
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font: $font_16_medium;
text-shadow: rgba(0, 0, 0, 0.5) 0 1px;
}
.preview {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
border-radius: ($upload_button_height * 0.5) !important;
background: 50% 50% no-repeat;
background-size: cover;
will-change: transform;
display: flex;
align-items: center;
flex-direction: row;
justify-content: flex-end;
}
.button {
width: $upload_button_height;
flex: 0 0 $upload_button_height;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset rgba(255, 255, 255, 0.05) 1px 1px, rgba(0, 0, 0, 0.3) -1px 0;
border-radius: $upload_button_height;
background: $content_bg_lighter;
&:hover {
svg {
fill: $color_danger;
}
}
svg {
width: 32px;
height: 32px;
}
}

View file

@ -1,25 +0,0 @@
import React, { FC } from 'react';
import { ImageGrid } from '~/components/editors/ImageGrid';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { NodeEditorProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { values } from '~/utils/ramda';
import styles from './styles.module.scss';
type IProps = NodeEditorProps;
const ImageEditor: FC<IProps> = () => {
const { pending, files, setFiles, uploadFiles } = useUploaderContext()!;
return (
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid files={files} setFiles={setFiles} locked={values(pending)} />
</div>
</UploadDropzone>
);
};
export { ImageEditor };

View file

@ -1,9 +0,0 @@
@import 'src/styles/variables';
.wrap {
min-height: 200px;
padding-bottom: $upload_button_height + $gap;
}
div.dropzone {
}

View file

@ -1,42 +0,0 @@
import React, { FC, useCallback } from 'react';
import { SortableImageGrid } from '~/components/sortable/SortableImageGrid';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { UploadStatus } from '~/store/uploader/UploaderStore';
import { IFile } from '~/types';
interface IProps {
files: IFile[];
setFiles: (val: IFile[]) => void;
locked: UploadStatus[];
}
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
const { isTablet } = useWindowSize();
const onMove = useCallback(
(newFiles: IFile[]) => {
setFiles(newFiles.filter((it) => it));
},
[setFiles],
);
const onDrop = useCallback(
(id: IFile['id']) => {
setFiles(files.filter((file) => file && file.id !== id));
},
[setFiles, files],
);
return (
<SortableImageGrid
onDelete={onDrop}
onSortEnd={onMove}
items={files}
locked={locked}
size={!isTablet ? 220 : (innerWidth - 60) / 2}
/>
);
};
export { ImageGrid };

View file

@ -1,5 +0,0 @@
@import "src/styles/variables";
.helper {
opacity: 0.5;
}

View file

@ -1,42 +0,0 @@
import { FC } from 'react';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { NodeEditorProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { AudioGrid } from '../AudioGrid';
import { ImageGrid } from '../ImageGrid';
import styles from './styles.module.scss';
const RoomEditor: FC<NodeEditorProps> = () => {
const {
filesImages,
filesAudios,
uploadFiles,
setImages,
setAudios,
pendingAudios,
pendingImages,
} = useUploaderContext()!;
return (
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid
files={filesImages}
setFiles={setImages}
locked={pendingImages}
/>
<AudioGrid
files={filesAudios}
setFiles={setAudios}
locked={pendingAudios}
/>
</div>
</UploadDropzone>
);
};
export { RoomEditor };

View file

@ -1,6 +0,0 @@
@import "src/styles/variables";
.wrap {
padding-bottom: $upload_button_height + $gap;
min-height: 200px;
}

View file

@ -1,30 +0,0 @@
import React, { FC, useCallback } from 'react';
import { Textarea } from '~/components/input/Textarea';
import { useRandomPhrase } from '~/constants/phrases';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { NodeEditorProps } from '~/types/node';
import { path } from '~/utils/ramda';
import styles from './styles.module.scss';
type IProps = NodeEditorProps & {};
const TextEditor: FC<IProps> = () => {
const { values, setFieldValue } = useNodeFormContext();
const placeholder = useRandomPhrase('SIMPLE');
const setText = useCallback((text: string) => setFieldValue('blocks', [{ type: 'text', text }]), [
setFieldValue,
]);
const text = (path(['blocks', 0, 'text'], values) as string) || '';
return (
<div className={styles.wrap}>
<Textarea value={text} handler={setText} minRows={6} placeholder={placeholder} />
</div>
);
};
export { TextEditor };

View file

@ -1,7 +0,0 @@
@import "src/styles/variables";
.wrap {
& > div {
padding-bottom: 64px;
}
}

View file

@ -1,37 +0,0 @@
import React, { FC, useCallback, useMemo } from 'react';
import classnames from 'classnames';
import { InputText } from '~/components/input/InputText';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { NodeEditorProps } from '~/types/node';
import { getYoutubeThumb } from '~/utils/dom';
import { path } from '~/utils/ramda';
import styles from './styles.module.scss';
type IProps = NodeEditorProps & {};
const VideoEditor: FC<IProps> = () => {
const { values, setFieldValue } = useNodeFormContext();
const setUrl = useCallback((url: string) => setFieldValue('blocks', [{ type: 'video', url }]), [
setFieldValue,
]);
const url = (path(['blocks', 0, 'url'], values) as string) || '';
const preview = useMemo(() => getYoutubeThumb(url), [url]);
const backgroundImage = (preview && `url("${preview}")`) || '';
return (
<div className={styles.preview} style={{ backgroundImage }}>
<div className={styles.input_wrap}>
<div className={classnames(styles.input, { active: !!preview })}>
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
</div>
</div>
</div>
);
};
export { VideoEditor };

View file

@ -1,38 +0,0 @@
@import 'src/styles/variables';
.preview {
padding-top: 56.25%;
position: relative;
border-radius: $radius;
background: 50% 50% no-repeat;
background-size: cover;
}
.input_wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.input {
// @include outer_shadow();
flex: 1 0 50%;
padding: $gap * 2;
border-radius: $radius;
background: $content_bg;
margin: 20px;
input {
text-align: center;
}
&:global(.active) {
background: $color_danger;
}
}

View file

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/input/Icon';
import { Toggle } from '~/components/input/Toggle';
import { FlowDisplayVariant } from '~/types';
@ -60,7 +60,11 @@ const FlowCellMenu: FC<Props> = ({
</div>
{hasDescription && (
<Group className={styles.description} horizontal onClick={toggleViewDescription}>
<Group
className={styles.description}
horizontal
onClick={toggleViewDescription}
>
<Toggle color="white" value={descriptionEnabled} />
<span>Текст</span>
</Group>

View file

@ -2,7 +2,7 @@ import React, { FC, ReactElement } from 'react';
import classNames from 'classnames';
import { Markdown } from '~/components/containers/Markdown';
import { Markdown } from '~/components/common/Markdown';
import { formatText } from '~/utils/dom';
import { DivProps } from '~/utils/types';

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Button } from '~/components/input/Button';
import { Dialog } from '~/constants/modal';
import { useWindowSize } from '~/hooks/dom/useWindowSize';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
import { InfiniteScroll } from '~/components/common/InfiniteScroll';
import { Icon } from '~/components/input/Icon';
import { INode } from '~/types';
@ -15,7 +15,12 @@ interface IProps {
onLoadMore: () => void;
}
const FlowSearchResults: FC<IProps> = ({ results, isLoading, onLoadMore, hasMore }) => {
const FlowSearchResults: FC<IProps> = ({
results,
isLoading,
onLoadMore,
hasMore,
}) => {
if (!results.length) {
return (
<div className={styles.loading}>
@ -28,7 +33,7 @@ const FlowSearchResults: FC<IProps> = ({ results, isLoading, onLoadMore, hasMore
return (
<div className={styles.wrap}>
<InfiniteScroll hasMore={hasMore} loadMore={onLoadMore}>
{results.map(node => (
{results.map((node) => (
<FlowRecentItem node={node} key={node.id} />
))}
</InfiniteScroll>

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { LabSquare } from '~/components/lab/LabSquare';
import styles from './styles.module.scss';
@ -15,8 +15,8 @@ const LabBanner: FC<IProps> = () => (
<Group className={styles.content}>
<p>
<strong>
Всё, что происходит здесь &mdash; всего лишь эксперимент, о котором не узнает никто за
пределами Убежища.
Всё, что происходит здесь &mdash; всего лишь эксперимент, о котором
не узнает никто за пределами Убежища.
</strong>
</p>
</Group>

View file

@ -2,9 +2,9 @@ import React, { FC, useCallback } from 'react';
import classNames from 'classnames';
import { Filler } from '~/components/containers/Filler';
import { Grid } from '~/components/containers/Grid';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/common/Filler';
import { Grid } from '~/components/common/Grid';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/input/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { URLS } from '~/constants/urls';
@ -21,14 +21,24 @@ type Props = {
commentCount: number;
};
const LabBottomPanel: FC<Props> = ({ node, hasNewComments, commentCount, isLoading }) => {
const LabBottomPanel: FC<Props> = ({
node,
hasNewComments,
commentCount,
isLoading,
}) => {
const { push } = useNavigation();
const onClick = useCallback(() => push(URLS.NODE_URL(node.id)), [push, node.id]);
const onClick = useCallback(
() => push(URLS.NODE_URL(node.id)),
[push, node.id],
);
return (
<Group horizontal className={styles.wrap} onClick={onClick}>
<div className={styles.timestamp}>
<Placeholder active={isLoading}>{getPrettyDate(node.created_at)}</Placeholder>
<Placeholder active={isLoading}>
{getPrettyDate(node.created_at)}
</Placeholder>
</div>
<Filler />
@ -37,7 +47,9 @@ const LabBottomPanel: FC<Props> = ({ node, hasNewComments, commentCount, isLoadi
{commentCount > 0 && (
<Grid
horizontal
className={classNames(styles.comments, { [styles.active]: hasNewComments })}
className={classNames(styles.comments, {
[styles.active]: hasNewComments,
})}
>
<Icon icon={hasNewComments ? 'comment_new' : 'comment'} size={24} />
<span>{commentCount}</span>

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Markdown } from '~/components/containers/Markdown';
import { Markdown } from '~/components/common/Markdown';
import { Paragraph } from '~/components/placeholders/Paragraph';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';

View file

@ -2,7 +2,7 @@ import React, { useRef } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import styles from './styles.module.scss';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { SearchInput } from '~/components/input/SearchInput';
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
import { LabNodesSort } from '~/types/lab';

View file

@ -1,6 +1,6 @@
import React, { FC, useCallback } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/input/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { URLS } from '~/constants/urls';
@ -44,7 +44,9 @@ const LabHero: FC<IProps> = ({ node, isLoading }) => {
<div className={styles.content}>
<div className={styles.title}>{node.title}</div>
<div className={styles.description}>{getPrettyDate(node.created_at)}</div>
<div className={styles.description}>
{getPrettyDate(node.created_at)}
</div>
</div>
</Group>
);

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { LabHero } from '~/components/lab/LabHero';
import styles from '~/containers/lab/LabStats/styles.module.scss';
import { INode } from '~/types';
@ -16,7 +16,7 @@ const LabHeroes: FC<IProps> = ({ nodes, isLoading }) => {
if (isLoading) {
return (
<Group className={styles.heroes}>
{empty.map(i => (
{empty.map((i) => (
<LabHero isLoading key={i} />
))}
</Group>
@ -25,7 +25,7 @@ const LabHeroes: FC<IProps> = ({ nodes, isLoading }) => {
return (
<Group className={styles.heroes}>
{nodes.slice(0, 10).map(node => (
{nodes.slice(0, 10).map((node) => (
<LabHero node={node} key={node?.id} />
))}
</Group>

View file

@ -1,6 +1,6 @@
import React, { VFC } from 'react';
import { Card } from '~/components/containers/Card';
import { Card } from '~/components/common/Card';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';

View file

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import Tippy from '@tippyjs/react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/input/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { NodeComponentProps } from '~/constants/node';

View file

@ -1,6 +1,6 @@
import React, { FC, useMemo } from 'react';
import { Markdown } from '~/components/containers/Markdown';
import { Markdown } from '~/components/common/Markdown';
import { Paragraph } from '~/components/placeholders/Paragraph';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';

View file

@ -1,7 +1,7 @@
import { FC } from 'react';
import { Avatar } from '~/components/common/Avatar';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { imagePresets } from '~/constants/urls';
import { IFile } from '~/types';
import { getURL } from '~/utils/dom';

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { Avatar } from '~/components/common/Avatar';
import { Card } from '~/components/containers/Card';
import { Card } from '~/components/common/Card';
import { useUserDescription } from '~/hooks/auth/useUserDescription';
import { INodeUser } from '~/types';

View file

@ -2,7 +2,7 @@ import React, { FC, useMemo } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { ERRORS } from '~/constants/errors';
import { t } from '~/utils/trans';

View file

@ -1,8 +1,8 @@
import React, { FC, ReactElement } from 'react';
import { Group } from '~/components/common/Group';
import { Hoverable } from '~/components/common/Hoverable';
import { SubTitle } from '~/components/common/SubTitle';
import { Group } from '~/components/containers/Group';
import { NodeThumbnail } from '~/components/node/NodeThumbnail';
import { INode } from '~/types';

View file

@ -2,7 +2,7 @@ import React, { FC, memo } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import cell_style from '~/components/node/NodeThumbnail/styles.module.scss';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { range } from '~/utils/ramda';

View file

@ -1,8 +1,8 @@
import React, { useCallback, useState, VFC } from 'react';
import { Card } from '~/components/containers/Card';
import { Markdown } from '~/components/containers/Markdown';
import { Padder } from '~/components/containers/Padder';
import { Card } from '~/components/common/Card';
import { Markdown } from '~/components/common/Markdown';
import { Padder } from '~/components/common/Padder';
import { NoteMenu } from '~/components/notes/NoteMenu';
import { formatText, getPrettyDate } from '~/utils/dom';

View file

@ -3,9 +3,9 @@ import { FC, useCallback } from 'react';
import { FormikConfig, useFormik } from 'formik';
import { Asserts, object, string } from 'yup';
import { Card } from '~/components/containers/Card';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Card } from '~/components/common/Card';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { Textarea } from '~/components/input/Textarea';
import { useRandomPhrase } from '~/constants/phrases';

View file

@ -1,8 +1,8 @@
import React, { FC, useCallback } from 'react';
import { Card } from '~/components/containers/Card';
import { Group } from '~/components/containers/Group';
import { Zone } from '~/components/containers/Zone';
import { Card } from '~/components/common/Card';
import { Group } from '~/components/common/Group';
import { Zone } from '~/components/common/Zone';
import { Button } from '~/components/input/Button';
import { InputRow } from '~/components/input/InputRow';
import { Toggle } from '~/components/input/Toggle';

View file

@ -1,7 +1,10 @@
import React, { FC, useMemo } from 'react';
import { Group } from '~/components/containers/Group';
import { Placeholder, PlaceholderProps } from '~/components/placeholders/Placeholder';
import { Group } from '~/components/common/Group';
import {
Placeholder,
PlaceholderProps,
} from '~/components/placeholders/Placeholder';
import styles from './styles.module.scss';
@ -14,17 +17,21 @@ const Paragraph: FC<Props> = ({ lines = 3, wordsLimit = 12, ...props }) => {
const iters = useMemo(
() =>
[...new Array(lines)].map(() =>
[...new Array(Math.ceil(Math.random() * wordsLimit))].map((_, i) => i)
[...new Array(Math.ceil(Math.random() * wordsLimit))].map((_, i) => i),
),
[lines, wordsLimit]
[lines, wordsLimit],
);
return (
<Group>
{iters.map((words, i) => (
<div className={styles.para} key={i}>
{words.map(word => (
<Placeholder key={word} width={`${Math.round(Math.random() * 120) + 60}px`} active />
{words.map((word) => (
<Placeholder
key={word}
width={`${Math.round(Math.random() * 120) + 60}px`}
active
/>
))}
</div>
))}

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { ERROR_LITERAL } from '~/constants/errors';

View file

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { ProfileLoader } from '~/containers/profile/ProfileLoader';
import { useUser } from '~/hooks/auth/useUser';
import markdown from '~/styles/common/markdown.module.scss';

View file

@ -1,8 +1,8 @@
import { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Padder } from '~/components/common/Padder';
import { Button } from '~/components/input/Button';
import { UserSettingsView } from '~/containers/settings/UserSettingsView';
import {

View file

@ -1,6 +1,6 @@
import { useState, VFC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
import { useStackContext } from '~/components/sidebar/SidebarStack';

View file

@ -1,6 +1,6 @@
import { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Button } from '~/components/input/Button';
import { useStackContext } from '~/components/sidebar/SidebarStack';
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';

View file

@ -2,8 +2,8 @@ import { VFC } from 'react';
import Link from 'next/link';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { VerticalMenu } from '~/components/menu/VerticalMenu';
import { URLS } from '~/constants/urls';
@ -28,7 +28,9 @@ const SettingsMenu: VFC<SettingsMenuProps> = () => (
</Link>
<Link href={URLS.SETTINGS.TRASH} passHref>
<VerticalMenu.Item onClick={console.log}>Удалённые посты</VerticalMenu.Item>
<VerticalMenu.Item onClick={console.log}>
Удалённые посты
</VerticalMenu.Item>
</Link>
</VerticalMenu>

View file

@ -1,6 +1,6 @@
import React, { FC, ReactNode, useMemo } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';

View file

@ -2,7 +2,7 @@ import { FC } from 'react';
import classNames from 'classnames';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Container } from '~/containers/main/Container';
import { useAuth } from '~/hooks/auth/useAuth';
import markdown from '~/styles/common/markdown.module.scss';