1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46: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,13 +1,12 @@
import React, { FC } from 'react';
import { Grid } from '~/components/containers/Grid';
import { Group } from '~/components/containers/Group';
import { Grid } from '~/components/common/Grid';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { OAuthProvider } from '~/types/auth';
import styles from './styles.module.scss';
interface IProps {
openOauthWindow: (provider: OAuthProvider) => void;
}

View file

@ -1,9 +1,9 @@
import { FC, useCallback, useRef } from 'react';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
import { DialogTitle } from '~/components/dialogs/DialogTitle';
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
import { DialogTitle } from '~/components/common/DialogTitle';
import { Group } from '~/components/common/Group';
import { Padder } from '~/components/common/Padder';
import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText';
import { Dialog } from '~/constants/modal';

View file

@ -1,10 +1,10 @@
import React, { FC, useCallback, useState } from 'react';
import { apiLoginWithSocial } from '~/api/auth';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
import { DialogTitle } from '~/components/dialogs/DialogTitle';
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
import { DialogTitle } from '~/components/common/DialogTitle';
import { Group } from '~/components/common/Group';
import { Padder } from '~/components/common/Padder';
import { InputText } from '~/components/input/InputText';
import { Toggle } from '~/components/input/Toggle';
import { getRandomPhrase } from '~/constants/phrases';

View file

@ -1,6 +1,6 @@
import React, { VFC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
@ -12,7 +12,10 @@ interface RestoreInvalidCodeProps {
onClose: () => void;
}
const RestoreInvalidCode: VFC<RestoreInvalidCodeProps> = ({ error, onClose }) => (
const RestoreInvalidCode: VFC<RestoreInvalidCodeProps> = ({
error,
onClose,
}) => (
<Group className={styles.error_shade}>
<Icon icon="close" size={64} />

View file

@ -1,6 +1,6 @@
import React, { VFC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback, useMemo, useState } from 'react';
import { apiRestoreCode } from '~/api/auth';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText';
import { useCloseOnEscape } from '~/hooks';
@ -9,7 +9,7 @@ import { useRestoreCode } from '~/hooks/auth/useRestoreCode';
import { useRestorePasswordForm } from '~/hooks/auth/useRestorePasswordForm';
import { DialogComponentProps } from '~/types/modal';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
import { RestoreInvalidCode } from './components/RestoreInvalidCode';
import { RestoreSuccess } from './components/RestoreSuccess';

View file

@ -1,6 +1,6 @@
import React, { VFC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';

View file

@ -1,14 +1,14 @@
import React, { useCallback, useMemo, useState, VFC } from 'react';
import { apiRequestRestoreCode } from '~/api/auth';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText';
import { useCloseOnEscape } from '~/hooks';
import { useRestoreRequestForm } from '~/hooks/auth/useRestoreRequestForm';
import { DialogComponentProps } from '~/types/modal';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
import { RestoreSent } from './components/RestoreSent';
import styles from './styles.module.scss';

View file

@ -2,12 +2,12 @@ import React, { FC, useCallback, useMemo } from 'react';
import { TelegramUser } from '@v9v/ts-react-telegram-login';
import { Padder } from '~/components/containers/Padder';
import { Padder } from '~/components/common/Padder';
import { Button } from '~/components/input/Button';
import { useTelegramAccount } from '~/hooks/auth/useTelegramAccount';
import { DialogComponentProps } from '~/types/modal';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
import { TelegramLoginForm } from '../LoginDialog/components/TelegramLoginForm/index';
interface TelegramAttachDialogProps extends DialogComponentProps {}

View file

@ -1,6 +1,6 @@
import { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Footer } from '~/components/main/Footer';
import { NodeNoComments } from '~/components/node/NodeNoComments';
import { NodeCommentFormSSR } from '~/containers/node/NodeCommentForm/ssr';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Padder } from '~/components/containers/Padder';
import { Padder } from '~/components/common/Padder';
import { Button } from '~/components/input/Button';
import { BorisContactItem } from './components/BorisContactItem';

View file

@ -2,10 +2,10 @@ import React, { FC, ReactNode } from 'react';
import classNames from 'classnames';
import { Card, CardProps } from '~/components/common/Card';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { SubTitle } from '~/components/common/SubTitle';
import { Card, CardProps } from '~/components/containers/Card';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import styles from './styles.module.scss';

View file

@ -3,7 +3,7 @@ import React, { VFC } from 'react';
import classNames from 'classnames';
import { addYears, differenceInMonths, differenceInYears } from 'date-fns';
import { CardProps } from '~/components/containers/Card';
import { CardProps } from '~/components/common/Card';
import { StatsCard } from '../StatsCard';

View file

@ -1,7 +1,7 @@
import React, { VFC } from 'react';
import { CardProps } from '~/components/containers/Card';
import { Filler } from '~/components/containers/Filler';
import { CardProps } from '~/components/common/Card';
import { Filler } from '~/components/common/Filler';
import { BasicCurveChart } from '../BasicCurveChart';
import { StatsCard } from '../StatsCard';

View file

@ -1,6 +1,6 @@
import { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
import { useTelegramAccount } from '~/hooks/auth/useTelegramAccount';
import { BorisUsageStats } from '~/types/boris';

View file

@ -1,8 +1,8 @@
import { FC } from 'react';
import { Anchor } from '~/components/common/Anchor';
import { Card } from '~/components/containers/Card';
import { Group } from '~/components/containers/Group';
import { Card } from '~/components/common/Card';
import { Group } from '~/components/common/Group';
import markdown from '~/styles/common/markdown.module.scss';
export interface BorisSuperpowersProps {}

View file

@ -3,7 +3,7 @@ import { FC, useCallback, useState } from 'react';
import { FormikProvider } from 'formik';
import { observer } from 'mobx-react-lite';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Button } from '~/components/input/Button';
import { ERROR_LITERAL } from '~/constants/errors';
import { EMPTY_COMMENT } from '~/constants/node';

View file

@ -0,0 +1,42 @@
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

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

View file

@ -0,0 +1,52 @@
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

@ -0,0 +1,18 @@
import React, { FC } from 'react';
import { UploadType } from '~/constants/uploads';
import { EditorUploadButton } from '~/containers/dialogs/EditorDialog/components/EditorButtons/components/EditorActionsPanel/components/EditorUploadButton';
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

@ -0,0 +1,12 @@
import React, { FC } from 'react';
import { Filler } from '~/components/common/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

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

View file

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

View file

@ -0,0 +1,45 @@
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

@ -0,0 +1,63 @@
@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

@ -0,0 +1,59 @@
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

@ -0,0 +1,27 @@
@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

@ -0,0 +1,74 @@
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

@ -0,0 +1,83 @@
@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

@ -0,0 +1,44 @@
import { FC } from 'react';
import { NODE_TYPES } from '~/constants/node';
import { IEditorComponentProps } from '~/types/node';
import { EditorAudioUploadButton } from '../components/EditorAudioUploadButton';
import { EditorFiller } from '../components/EditorFiller';
import { EditorImageUploadButton } from '../components/EditorImageUploadButton';
import { EditorPublicSwitch } from '../components/EditorPublicSwitch';
import { EditorUploadCoverButton } from '../components/EditorUploadCoverButton';
export const NODE_PANEL_COMPONENTS: Record<
string,
FC<IEditorComponentProps>[]
> = {
[NODE_TYPES.TEXT]: [
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.VIDEO]: [
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.IMAGE]: [
EditorImageUploadButton,
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.AUDIO]: [
EditorAudioUploadButton,
EditorImageUploadButton,
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.ROOM]: [
EditorAudioUploadButton,
EditorImageUploadButton,
EditorFiller,
],
};

View file

@ -0,0 +1,26 @@
import React, { createElement, FC } from 'react';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { has } from '~/utils/ramda';
import { NODE_PANEL_COMPONENTS } from './constants';
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

@ -0,0 +1,30 @@
@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

@ -0,0 +1,45 @@
import React, { FC } from 'react';
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 { InputText } from '~/components/input/InputText';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { EditorActionsPanel } from './components/EditorActionsPanel';
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

@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { Group } from '~/components/common/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

@ -0,0 +1,42 @@
@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

@ -0,0 +1,26 @@
import React, { FC } from 'react';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { NodeEditorProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { values } from '~/utils/ramda';
import { ImageGrid } from '../ImageGrid';
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

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

View file

@ -0,0 +1,42 @@
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

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

View file

@ -0,0 +1,42 @@
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

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

View file

@ -0,0 +1,30 @@
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

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

View file

@ -0,0 +1,37 @@
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

@ -0,0 +1,38 @@
@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

@ -0,0 +1,31 @@
import { FC } from 'react';
import { NODE_TYPES } from '~/constants/node';
import { INode } from '~/types';
import { NodeEditorProps } from '~/types/node';
import { AudioEditor } from '../components/AudioEditor';
import { ImageEditor } from '../components/ImageEditor';
import { RoomEditor } from '../components/RoomEditor';
import { TextEditor } from '../components/TextEditor';
import { VideoEditor } from '../components/VideoEditor';
export const NODE_EDITORS: Record<
typeof NODE_TYPES[keyof typeof NODE_TYPES],
FC<NodeEditorProps>
> = {
[NODE_TYPES.IMAGE]: ImageEditor,
[NODE_TYPES.TEXT]: TextEditor,
[NODE_TYPES.VIDEO]: VideoEditor,
[NODE_TYPES.AUDIO]: AudioEditor,
[NODE_TYPES.ROOM]: RoomEditor,
};
export const NODE_EDITOR_DATA: Record<
typeof NODE_TYPES[keyof typeof NODE_TYPES],
Partial<INode>
> = {
[NODE_TYPES.TEXT]: {
blocks: [{ text: '', type: 'text' }],
},
};

View file

@ -1,13 +1,16 @@
import React, { createElement, FC, useCallback, useMemo, useState } from 'react';
import React, {
createElement,
FC,
useCallback,
useMemo,
useState,
} from 'react';
import { FormikProvider } from 'formik';
import { observer } from 'mobx-react-lite';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
import { EditorButtons } from '~/components/editors/EditorButtons';
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
import { NODE_EDITORS } from '~/constants/node';
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
import { CoverBackdrop } from '~/components/common/CoverBackdrop';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { useCloseOnEscape } from '~/hooks';
import { useUploader } from '~/hooks/data/useUploader';
@ -17,6 +20,9 @@ import { DialogComponentProps } from '~/types/modal';
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
import { prop } from '~/utils/ramda';
import { EditorButtons } from './components/EditorButtons';
import { EditorConfirmClose } from './components/EditorConfirmClose';
import { NODE_EDITORS } from './constants/';
import styles from './styles.module.scss';
interface Props extends DialogComponentProps {
@ -24,61 +30,73 @@ interface Props extends DialogComponentProps {
onSubmit: (node: INode) => Promise<unknown>;
}
const EditorDialog: FC<Props> = observer(({ node, onRequestClose, onSubmit }) => {
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
const EditorDialog: FC<Props> = observer(
({ node, onRequestClose, onSubmit }) => {
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
const uploader = useUploader(UploadSubject.Editor, UploadTarget.Nodes, node.files);
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
const { values, handleSubmit, dirty } = formik;
const uploader = useUploader(
UploadSubject.Editor,
UploadTarget.Nodes,
node.files,
);
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
const { values, handleSubmit, dirty } = formik;
const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
const component = useMemo(
() => node.type && prop(node.type, NODE_EDITORS),
[node.type],
);
const closeConfirmModal = useCallback(() => {
setConfirmModalShown(false);
}, [setConfirmModalShown]);
const closeConfirmModal = useCallback(() => {
setConfirmModalShown(false);
}, [setConfirmModalShown]);
const onClose = useCallback(() => {
if (!dirty) {
onRequestClose();
return;
const onClose = useCallback(() => {
if (!dirty) {
onRequestClose();
return;
}
if (isConfirmModalShown) {
closeConfirmModal();
return;
}
setConfirmModalShown(true);
}, [dirty, isConfirmModalShown, onRequestClose, closeConfirmModal]);
useCloseOnEscape(onClose);
if (!component) {
return null;
}
if (isConfirmModalShown) {
closeConfirmModal();
return;
}
return (
<UploaderContextProvider value={uploader}>
<FormikProvider value={formik}>
<form onSubmit={handleSubmit} className={styles.form}>
<BetterScrollDialog
footer={<EditorButtons />}
backdrop={<CoverBackdrop cover={values.cover} />}
width={860}
onClose={onClose}
>
<>
{isConfirmModalShown && (
<EditorConfirmClose
onApprove={onRequestClose}
onDecline={closeConfirmModal}
/>
)}
setConfirmModalShown(true);
}, [dirty, isConfirmModalShown, onRequestClose, closeConfirmModal]);
useCloseOnEscape(onClose);
if (!component) {
return null;
}
return (
<UploaderContextProvider value={uploader}>
<FormikProvider value={formik}>
<form onSubmit={handleSubmit} className={styles.form}>
<BetterScrollDialog
footer={<EditorButtons />}
backdrop={<CoverBackdrop cover={values.cover} />}
width={860}
onClose={onClose}
>
<>
{isConfirmModalShown && (
<EditorConfirmClose onApprove={onRequestClose} onDecline={closeConfirmModal} />
)}
<div className={styles.editor}>{createElement(component)}</div>
</>
</BetterScrollDialog>
</form>
</FormikProvider>
</UploaderContextProvider>
);
});
<div className={styles.editor}>{createElement(component)}</div>
</>
</BetterScrollDialog>
</form>
</FormikProvider>
</UploaderContextProvider>
);
},
);
export { EditorDialog };

View file

@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import { observer } from 'mobx-react-lite';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { ModalWrapper } from '~/components/common/ModalWrapper';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { useLoadNode } from '~/hooks/node/useLoadNode';
@ -16,29 +16,37 @@ export interface EditorEditDialogProps extends DialogComponentProps {
nodeId: number;
}
const EditorEditDialog: FC<EditorEditDialogProps> = observer(({ nodeId, onRequestClose }) => {
const { node, isLoading } = useLoadNode(nodeId);
const updateNode = useUpdateNode(nodeId);
const EditorEditDialog: FC<EditorEditDialogProps> = observer(
({ nodeId, onRequestClose }) => {
const { node, isLoading } = useLoadNode(nodeId);
const updateNode = useUpdateNode(nodeId);
const onSubmit = useCallback(
async (node: INode) => {
await updateNode(node);
onRequestClose();
},
[updateNode, onRequestClose]
);
if (isLoading || !node) {
return (
<ModalWrapper onOverlayClick={onRequestClose}>
<div className={styles.loader}>
<LoaderCircle size={64} />
</div>
</ModalWrapper>
const onSubmit = useCallback(
async (node: INode) => {
await updateNode(node);
onRequestClose();
},
[updateNode, onRequestClose],
);
}
return <EditorDialog node={node} onRequestClose={onRequestClose} onSubmit={onSubmit} />;
});
if (isLoading || !node) {
return (
<ModalWrapper onOverlayClick={onRequestClose}>
<div className={styles.loader}>
<LoaderCircle size={64} />
</div>
</ModalWrapper>
);
}
return (
<EditorDialog
node={node}
onRequestClose={onRequestClose}
onSubmit={onSubmit}
/>
);
},
);
export { EditorEditDialog };

View file

@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { observer } from 'mobx-react-lite';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { ModalWrapper } from '~/components/common/ModalWrapper';
import { DIALOG_CONTENT } from '~/constants/modal';
import { useModalStore } from '~/store/modal/useModalStore';
import { has } from '~/utils/ramda';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
import styles from './styles.module.scss';

View file

@ -2,8 +2,8 @@ import React, { FC, FormEvent, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/common/Group';
import { Superpower } from '~/components/common/Superpower';
import { Group } from '~/components/containers/Group';
import { FlowRecent } from '~/components/flow/FlowRecent';
import { FlowSearchResults } from '~/components/flow/FlowSearchResults';
import { Icon } from '~/components/input/Icon';

View file

@ -1,7 +1,7 @@
import { FC, memo } from 'react';
import { Columns } from '~/components/common/Columns';
import { Hoverable } from '~/components/common/Hoverable';
import { Columns } from '~/components/containers/Columns';
import { LabNoResults } from '~/components/lab/LabNoResults';
import { LabNode } from '~/components/lab/LabNode';
import { useLabContext } from '~/utils/context/LabContextProvider';

View file

@ -1,6 +1,6 @@
import React, { FC, memo } from 'react';
import { Columns } from '~/components/containers/Columns';
import { Columns } from '~/components/common/Columns';
import { LabNode } from '~/components/lab/LabNode';
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
import { values } from '~/utils/ramda';

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { Group } from '~/components/common/Group';
import { SubTitle } from '~/components/common/SubTitle';
import { Group } from '~/components/containers/Group';
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
import { LabFactoryBanner } from '~/components/lab/LabFactoryBanner';
import { LabHeroes } from '~/components/lab/LabHeroes';

View file

@ -6,7 +6,7 @@ import { observer } from 'mobx-react-lite';
import { Anchor } from '~/components/common/Anchor';
import { Authorized } from '~/components/common/Authorized';
import { Filler } from '~/components/containers/Filler';
import { Filler } from '~/components/common/Filler';
import { Button } from '~/components/input/Button';
import { Logo } from '~/components/main/Logo';
import { Dialog } from '~/constants/modal';

View file

@ -1,7 +1,7 @@
import { FC, useMemo } from 'react';
import { Card } from '~/components/common/Card';
import { SubTitle } from '~/components/common/SubTitle';
import { Card } from '~/components/containers/Card';
import { Backlink } from '~/components/node/Backlink';
import { NodeBackLink } from '~/types';
import { has } from '~/utils/ramda';

View file

@ -1,9 +1,9 @@
import { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { Sticky } from '~/components/containers/Sticky';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Padder } from '~/components/common/Padder';
import { Sticky } from '~/components/common/Sticky';
import { NodeAuthorBlock } from '~/components/node/NodeAuthorBlock';
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
import { NodeNoComments } from '~/components/node/NodeNoComments';

View file

@ -10,7 +10,7 @@ import {
import classnames from 'classnames';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { AudioPlayer } from '~/components/media/AudioPlayer';
import { UploadType } from '~/constants/uploads';
import { IComment, IFile } from '~/types';

View file

@ -1,6 +1,6 @@
import { FC } from 'react';
import { Padder } from '~/components/containers/Padder';
import { Padder } from '~/components/common/Padder';
import { NotificationSettingsForm } from '~/components/notifications/NotificationSettingsForm';
import { useOAuth } from '~/hooks/auth/useOAuth';
import { useNotificationSettings } from '~/hooks/notifications/useNotificationSettings';

View file

@ -1,6 +1,6 @@
import React, { FC, Fragment } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { ProfileAvatar } from '~/components/profile/ProfileAvatar';
import { usePatchUser } from '~/hooks/auth/usePatchUser';
@ -31,7 +31,11 @@ const ProfileInfo: FC<IProps> = ({ isOwn }) => {
<div>
<Group className={styles.wrap} horizontal>
<div className={styles.avatar}>
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={photo} />
<ProfileAvatar
canEdit={isOwn}
onChangePhoto={updatePhoto}
photo={photo}
/>
</div>
<div className={styles.field}>

View file

@ -2,8 +2,8 @@ import React, { FC } from 'react';
import classNames from 'classnames';
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 { useUserActiveStatus } from '~/hooks/auth/useUserActiveStatus';
import { IUser } from '~/types/auth';
@ -23,7 +23,9 @@ const ProfileQuickInfo: FC<ProfileQuickInfoProps> = ({ user }) => {
<h5 className={styles.fullname}>{user.fullname || user.username}</h5>
<div className={styles.username}>~{user.username}</div>
<div className={classNames(styles.status, { [styles.active]: isActive })}>
<div
className={classNames(styles.status, { [styles.active]: isActive })}
>
{isActive ? 'юнит в сознании' : 'юнит деактивирован'}
</div>
</Filler>

View file

@ -1,7 +1,7 @@
import React, { VFC } from 'react';
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 { ProfileAvatar } from '~/components/profile/ProfileAvatar';
import { usePatchUser } from '~/hooks/auth/usePatchUser';
import { useUser } from '~/hooks/auth/useUser';
@ -16,11 +16,18 @@ const ProfileSidebarHead: VFC<ProfileSidebarHeadProps> = () => {
return (
<Group horizontal>
<ProfileAvatar canEdit onChangePhoto={updatePhoto} photo={user.photo} size={72} />
<ProfileAvatar
canEdit
onChangePhoto={updatePhoto}
photo={user.photo}
size={72}
/>
<Filler>
<div className={styles.name}>{user.fullname || user.username}</div>
<div className={styles.username}>{!!user.fullname && `~${user.username}`}</div>
<div className={styles.username}>
{!!user.fullname && `~${user.username}`}
</div>
</Filler>
</Group>
);

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { Group } from '~/components/common/Group';
import { Padder } from '~/components/common/Padder';
import { Button } from '~/components/input/Button';
import { MenuButton } from '~/components/menu/MenuButton';

View file

@ -2,9 +2,9 @@ import React, { useCallback, VFC } from 'react';
import classNames from 'classnames';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Zone } from '~/components/containers/Zone';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Zone } from '~/components/common/Zone';
import { VerticalMenu } from '~/components/menu/VerticalMenu';
import { useStackContext } from '~/components/sidebar/SidebarStack';
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';

View file

@ -1,9 +1,9 @@
import React, { VFC } from 'react';
import { Card } from '~/components/common/Card';
import { Grid } from '~/components/common/Grid';
import { Group } from '~/components/common/Group';
import { Square } from '~/components/common/Square';
import { Card } from '~/components/containers/Card';
import { Grid } from '~/components/containers/Grid';
import { Group } from '~/components/containers/Group';
interface ProfileStatsProps {}

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Tabs } from '~/components/dialogs/Tabs';
import { Tabs } from '~/components/common/Tabs';
import styles from './styles.module.scss';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Group } from '~/components/common/Group';
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
interface ProfileTogglesProps {}

View file

@ -1,8 +1,8 @@
import React, { VFC } 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 { FlowRecentItem } from '~/components/flow/FlowRecentItem';
import { Icon } from '~/components/input/Icon';
import { InputText } from '~/components/input/InputText';
@ -32,7 +32,7 @@ const SettingsDeleted: VFC<SettingsDeletedProps> = () => {
<br />
<div className={styles.grid}>
{nodes.map(node => (
{nodes.map((node) => (
<div className={styles.item} key={node.id}>
<FlowRecentItem node={node} key={node.id} />
</div>

View file

@ -1,7 +1,7 @@
import { FC, useCallback, useState, VFC } from 'react';
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 { NoteCard } from '~/components/notes/NoteCard';
import { NoteCreationForm } from '~/components/notes/NoteCreationForm';

View file

@ -2,8 +2,8 @@ import React, { FC } from 'react';
import classNames from 'classnames';
import { Card } from '~/components/containers/Card';
import { Group } from '~/components/containers/Group';
import { Card } from '~/components/common/Card';
import { Group } from '~/components/common/Group';
import { Theme, themeColors } from '~/constants/themes';
import { useTheme } from '~/utils/providers/ThemeProvider';

View file

@ -1,9 +1,9 @@
import { FC } from 'react';
import { Filler } from '~/components/common/Filler';
import { Group } from '~/components/common/Group';
import { Superpower } from '~/components/common/Superpower';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Zone } from '~/components/containers/Zone';
import { Zone } from '~/components/common/Zone';
import { InputText } from '~/components/input/InputText';
import { Textarea } from '~/components/input/Textarea';
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';

View file

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, VFC } from 'react';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { CoverBackdrop } from '~/components/common/CoverBackdrop';
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
import { ProfileSidebarNotifications } from '~/components/profile/ProfileSidebarNotifications';
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';

View file

@ -1,6 +1,6 @@
import { useMemo, VFC } from 'react';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
import { InfiniteScroll } from '~/components/common/InfiniteScroll';
import { Icon } from '~/components/input/Icon';
import { SidebarStack } from '~/components/sidebar/SidebarStack';
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';

View file

@ -1,6 +1,12 @@
import React, { FC, HTMLAttributes, useCallback, useMemo, useState } from 'react';
import React, {
FC,
HTMLAttributes,
useCallback,
useMemo,
useState,
} from 'react';
import { TagField } from '~/components/containers/TagField';
import { TagField } from '~/components/common/TagField';
import { Tag } from '~/components/tags/Tag';
import { TagInput } from '~/containers/tags/TagInput';
import { ITag } from '~/types';
@ -35,8 +41,10 @@ export const Tags: FC<IProps> = ({
return;
}
const exist = tags.map(tag => tag.title);
const uniqueTags = uniq([...exist, ...data, ...last]).filter(el => el) as string[];
const exist = tags.map((tag) => tag.title);
const uniqueTags = uniq([...exist, ...data, ...last]).filter(
(el) => el,
) as string[];
if (uniqueTags.length === exist.length) {
return;
@ -45,18 +53,18 @@ export const Tags: FC<IProps> = ({
onTagsChange(uniqueTags);
setData([]);
},
[data, onTagsChange, tags]
[data, onTagsChange, tags],
);
const onAppendTag = useCallback(
(created: string[]) => {
setData(
uniq([...data, ...created]).filter(
title => !tags.some(it => it.title?.trim() === title?.trim())
)
(title) => !tags.some((it) => it.title?.trim() === title?.trim()),
),
);
},
[data, setData, tags]
[data, setData, tags],
);
const onClearTag = useCallback((): string | undefined => {
@ -67,13 +75,16 @@ export const Tags: FC<IProps> = ({
}, [data, setData]);
const exclude = useMemo(
() => [...(data || []), ...(tags || []).filter(el => el.title).map(({ title }) => title!)],
[data, tags]
() => [
...(data || []),
...(tags || []).filter((el) => el.title).map(({ title }) => title!),
],
[data, tags],
);
return (
<TagField {...props}>
{catTags.map(tag => (
{catTags.map((tag) => (
<Tag
key={tag.title}
tag={tag}
@ -83,7 +94,7 @@ export const Tags: FC<IProps> = ({
/>
))}
{ordinaryTags.map(tag => (
{ordinaryTags.map((tag) => (
<Tag
key={tag.title}
tag={tag}
@ -93,7 +104,7 @@ export const Tags: FC<IProps> = ({
/>
))}
{data.map(title => (
{data.map((title) => (
<Tag key={title} tag={{ title }} editing />
))}