mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
Добавили заметки в сайдбар (#126)
* added notes sidebar * added note dropping and editing * added sidebar navigation * handling sidebarchanges over time * using router back for closing sidebar * fixed tripping inside single sidebar * added superpowers toggle to sidebar * user button opens sidebar now * added profile cover for profile sidebar * removed profile sidebar completely * ran prettier over project * added note not found error literal
This commit is contained in:
parent
fe3db608d6
commit
5d34090238
72 changed files with 1241 additions and 664 deletions
|
@ -1,10 +1,10 @@
|
|||
import { FC, memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FC, memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { debounce, throttle } from "throttle-debounce";
|
||||
import { debounce, throttle } from 'throttle-debounce';
|
||||
|
||||
import { useWindowSize } from "~/hooks/dom/useWindowSize";
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface LoginSceneProps {}
|
||||
|
||||
|
@ -17,31 +17,31 @@ interface Layer {
|
|||
|
||||
const layers: Layer[] = [
|
||||
{
|
||||
src: "/images/clouds__bg.svg",
|
||||
src: '/images/clouds__bg.svg',
|
||||
velocity: -0.3,
|
||||
width: 3840,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
src: "/images/clouds__cube.svg",
|
||||
src: '/images/clouds__cube.svg',
|
||||
velocity: -0.1,
|
||||
width: 3840,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
src: "/images/clouds__cloud.svg",
|
||||
src: '/images/clouds__cloud.svg',
|
||||
velocity: 0.2,
|
||||
width: 3840,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
src: "/images/clouds__dudes.svg",
|
||||
src: '/images/clouds__dudes.svg',
|
||||
velocity: 0.5,
|
||||
width: 3840,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
src: "/images/clouds__trash.svg",
|
||||
src: '/images/clouds__trash.svg',
|
||||
velocity: 0.8,
|
||||
width: 3840,
|
||||
height: 1080,
|
||||
|
@ -52,7 +52,7 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
|||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const imageRefs = useRef<Array<SVGImageElement | null>>([]);
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
const domRect = useRef<DOMRect>();
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
|
@ -84,11 +84,11 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
|||
|
||||
useEffect(() => {
|
||||
const listener = throttle(100, onMouseMove);
|
||||
document.addEventListener("mousemove", listener);
|
||||
return () => document.removeEventListener("mousemove", listener);
|
||||
document.addEventListener('mousemove', listener);
|
||||
return () => document.removeEventListener('mousemove', listener);
|
||||
}, []);
|
||||
|
||||
if (isMobile) {
|
||||
if (isTablet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -103,16 +103,16 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
|||
>
|
||||
<defs>
|
||||
<linearGradient id="fallbackGradient" x1={0} x2={0} y1={1} y2={0}>
|
||||
<stop style={{ stopColor: "#ffccaa", stopOpacity: 1 }} offset="0" />
|
||||
<stop style={{ stopColor: '#ffccaa', stopOpacity: 1 }} offset="0" />
|
||||
<stop
|
||||
style={{ stopColor: "#fff6d5", stopOpacity: 1 }}
|
||||
style={{ stopColor: '#fff6d5', stopOpacity: 1 }}
|
||||
offset="0.34655526"
|
||||
/>
|
||||
<stop
|
||||
style={{ stopColor: "#afc6e9", stopOpacity: 1 }}
|
||||
style={{ stopColor: '#afc6e9', stopOpacity: 1 }}
|
||||
offset="0.765342"
|
||||
/>
|
||||
<stop style={{ stopColor: "#879fde", stopOpacity: 1 }} offset="1" />
|
||||
<stop style={{ stopColor: '#879fde', stopOpacity: 1 }} offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { BorisContacts } from '~/components/boris/BorisContacts';
|
||||
import { BorisStats } from '~/components/boris/BorisStats';
|
||||
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import styles from '~/layouts/BorisLayout/styles.module.scss';
|
||||
import { BorisUsageStats } from '~/types/boris';
|
||||
|
||||
interface Props {
|
||||
isUser: boolean;
|
||||
isTester: boolean;
|
||||
stats: BorisUsageStats;
|
||||
setBetaTester: (val: boolean) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading, isTester, setBetaTester }) => (
|
||||
<Group className={styles.stats__container}>
|
||||
<div className={styles.super_powers}>
|
||||
{isUser && <BorisSuperpowers active={isTester} onChange={setBetaTester} />}
|
||||
</div>
|
||||
|
||||
<BorisContacts />
|
||||
|
||||
<div className={styles.stats__wrap}>
|
||||
<BorisStats stats={stats} isLoading={isLoading} />
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
export { BorisSidebar };
|
|
@ -1,15 +1,17 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const Superpower: FC<IProps> = ({ children }) => {
|
||||
const Superpower: FC<IProps> = observer(({ children }) => {
|
||||
const { isTester } = useAuth();
|
||||
|
||||
if (!isTester) return null;
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
});
|
||||
|
||||
export { Superpower };
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import React, { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
|
||||
import React, { DetailedHTMLProps, VFC, HTMLAttributes } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from '~/styles/common/markdown.module.scss';
|
||||
import { formatText } from '~/utils/dom';
|
||||
|
||||
interface IProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
|
||||
interface IProps
|
||||
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
children?: string;
|
||||
}
|
||||
|
||||
const Markdown: FC<IProps> = ({ className, ...props }) => (
|
||||
<div className={classNames(styles.wrapper, className)} {...props} />
|
||||
const Markdown: VFC<IProps> = ({ className, children = '', ...props }) => (
|
||||
<div
|
||||
className={classNames(styles.wrapper, className)}
|
||||
{...props}
|
||||
dangerouslySetInnerHTML={{ __html: formatText(children) }}
|
||||
/>
|
||||
);
|
||||
|
||||
export { Markdown };
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
import React, { FC } from "react";
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import classNames from "classnames";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface ZoneProps {
|
||||
title?: string;
|
||||
className?: string;
|
||||
color?: "danger" | "normal";
|
||||
color?: 'danger' | 'normal';
|
||||
}
|
||||
|
||||
const Zone: FC<ZoneProps> = ({
|
||||
title,
|
||||
className,
|
||||
children,
|
||||
color = "normal",
|
||||
color = 'normal',
|
||||
}) => (
|
||||
<div className={classNames(className, styles.pad, styles[color])}>
|
||||
<div
|
||||
className={classNames(className, styles.pad, styles[color], {
|
||||
[styles.with_title]: !!title,
|
||||
})}
|
||||
>
|
||||
{!!title && (
|
||||
<div className={styles.title}>
|
||||
<span>{title}</span>
|
||||
|
|
|
@ -8,7 +8,7 @@ $pad_usual: mix(white, $content_bg, 10%);
|
|||
|
||||
span {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
top: -$gap;
|
||||
left: $radius;
|
||||
transform: translate(0, -100%);
|
||||
background: $pad_usual;
|
||||
|
@ -25,7 +25,7 @@ $pad_usual: mix(white, $content_bg, 10%);
|
|||
}
|
||||
|
||||
.pad {
|
||||
padding: $gap * 1.5 $gap $gap;
|
||||
padding: $gap;
|
||||
box-shadow: inset $pad_usual 0 0 0 2px;
|
||||
border-radius: $radius;
|
||||
position: relative;
|
||||
|
@ -33,4 +33,8 @@ $pad_usual: mix(white, $content_bg, 10%);
|
|||
&.danger {
|
||||
box-shadow: inset $pad_danger 0 0 0 2px;
|
||||
}
|
||||
|
||||
&.with_title {
|
||||
padding-top: $gap * 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
|||
|
||||
const EditorButtons: FC = () => {
|
||||
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
return (
|
||||
<Padder style={{ position: 'relative' }}>
|
||||
|
@ -23,14 +23,14 @@ const EditorButtons: FC = () => {
|
|||
title="Название"
|
||||
value={values.title}
|
||||
handler={handleChange('title')}
|
||||
autoFocus={!isMobile}
|
||||
autoFocus={!isTablet}
|
||||
maxLength={256}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Filler>
|
||||
|
||||
<Button
|
||||
title={isMobile ? undefined : 'Сохранить'}
|
||||
title={isTablet ? undefined : 'Сохранить'}
|
||||
iconRight="check"
|
||||
color={values.is_promoted ? 'primary' : 'lab'}
|
||||
disabled={isSubmitting}
|
||||
|
|
|
@ -12,20 +12,20 @@ interface IProps {
|
|||
}
|
||||
|
||||
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
const onMove = useCallback(
|
||||
(newFiles: IFile[]) => {
|
||||
setFiles(newFiles.filter(it => it));
|
||||
},
|
||||
[setFiles, files]
|
||||
[setFiles, files],
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(id: IFile['id']) => {
|
||||
setFiles(files.filter(file => file && file.id !== id));
|
||||
},
|
||||
[setFiles, files]
|
||||
[setFiles, files],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -34,7 +34,7 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
|||
onSortEnd={onMove}
|
||||
items={files}
|
||||
locked={locked}
|
||||
size={!isMobile ? 220 : (innerWidth - 60) / 2}
|
||||
size={!isTablet ? 220 : (innerWidth - 60) / 2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -39,10 +39,12 @@ const FlowCell: FC<Props> = ({
|
|||
canEdit = false,
|
||||
onChangeCellView,
|
||||
}) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
const withText =
|
||||
((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text;
|
||||
((!!flow.display && flow.display !== 'single') || !image) &&
|
||||
flow.show_description &&
|
||||
!!text;
|
||||
const {
|
||||
hasDescription,
|
||||
setViewHorizontal,
|
||||
|
@ -51,14 +53,19 @@ const FlowCell: FC<Props> = ({
|
|||
setViewSingle,
|
||||
toggleViewDescription,
|
||||
} = useFlowCellControls(id, text, flow, onChangeCellView);
|
||||
const { isActive: isMenuActive, activate, ref, deactivate } = useClickOutsideFocus();
|
||||
const {
|
||||
isActive: isMenuActive,
|
||||
activate,
|
||||
ref,
|
||||
deactivate,
|
||||
} = useClickOutsideFocus();
|
||||
|
||||
const shadeSize = useMemo(() => {
|
||||
const min = isMobile ? 10 : 15;
|
||||
const max = isMobile ? 20 : 40;
|
||||
const min = isTablet ? 10 : 15;
|
||||
const max = isTablet ? 20 : 40;
|
||||
|
||||
return withText ? min : max;
|
||||
}, [withText, isMobile]);
|
||||
}, [withText, isTablet]);
|
||||
|
||||
const shadeAngle = useMemo(() => {
|
||||
if (flow.display === 'vertical') {
|
||||
|
@ -73,7 +80,10 @@ const FlowCell: FC<Props> = ({
|
|||
}, [flow.display]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.cell, styles[flow.display || 'single'])} ref={ref as any}>
|
||||
<div
|
||||
className={classNames(styles.cell, styles[flow.display || 'single'])}
|
||||
ref={ref as any}
|
||||
>
|
||||
{canEdit && !isMenuActive && (
|
||||
<div className={styles.menu}>
|
||||
<MenuDots onClick={activate} />
|
||||
|
@ -98,7 +108,10 @@ const FlowCell: FC<Props> = ({
|
|||
|
||||
<Anchor className={styles.link} href={to}>
|
||||
{withText && (
|
||||
<FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
|
||||
<FlowCellText
|
||||
className={styles.text}
|
||||
heading={<h4 className={styles.title}>{title}</h4>}
|
||||
>
|
||||
{text!}
|
||||
</FlowCellText>
|
||||
)}
|
||||
|
@ -113,7 +126,12 @@ const FlowCell: FC<Props> = ({
|
|||
)}
|
||||
|
||||
{!!title && (
|
||||
<CellShade color={color} className={styles.shade} size={shadeSize} angle={shadeAngle} />
|
||||
<CellShade
|
||||
color={color}
|
||||
className={styles.shade}
|
||||
size={shadeSize}
|
||||
angle={shadeAngle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!withText && (
|
||||
|
|
|
@ -8,7 +8,6 @@ import { DivProps } from '~/utils/types';
|
|||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
||||
interface Props extends DivProps {
|
||||
children: string;
|
||||
heading: string | ReactElement;
|
||||
|
@ -17,10 +16,7 @@ interface Props extends DivProps {
|
|||
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => (
|
||||
<div {...rest} className={classNames(styles.text, rest.className)}>
|
||||
{heading && <div className={styles.heading}>{heading}</div>}
|
||||
<Markdown
|
||||
className={styles.description}
|
||||
dangerouslySetInnerHTML={{ __html: formatText(children) }}
|
||||
/>
|
||||
<Markdown className={styles.description}>{formatText(children)}</Markdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -34,14 +34,17 @@ const lazy = {
|
|||
};
|
||||
|
||||
export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
const { push } = useNavigation();
|
||||
|
||||
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
|
||||
const [controlledSwiper, setControlledSwiper] = useState<
|
||||
SwiperClass | undefined
|
||||
>(undefined);
|
||||
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
||||
const preset = useMemo(() => (isMobile ? ImagePresets.cover : ImagePresets.small_hero), [
|
||||
isMobile,
|
||||
]);
|
||||
const preset = useMemo(
|
||||
() => (isTablet ? ImagePresets.cover : ImagePresets.small_hero),
|
||||
[isTablet],
|
||||
);
|
||||
|
||||
const onNext = useCallback(() => {
|
||||
controlledSwiper?.slideNext(1);
|
||||
|
@ -79,7 +82,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
|||
(sw: SwiperClass) => {
|
||||
push(URLS.NODE_URL(heroes[sw.realIndex]?.id));
|
||||
},
|
||||
[push, heroes]
|
||||
[push, heroes],
|
||||
);
|
||||
|
||||
if (!heroes.length) {
|
||||
|
|
|
@ -20,11 +20,9 @@ const LabDescription: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
|||
<Paragraph />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown
|
||||
className={styles.wrap}
|
||||
dangerouslySetInnerHTML={{ __html: formatText(node.description) }}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Markdown className={styles.wrap} onClick={onClick}>
|
||||
{formatText(node.description)}
|
||||
</Markdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,9 +10,10 @@ import { path } from '~/utils/ramda';
|
|||
import styles from './styles.module.scss';
|
||||
|
||||
const LabText: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
||||
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [
|
||||
node,
|
||||
]);
|
||||
const content = useMemo(
|
||||
() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''),
|
||||
[node],
|
||||
);
|
||||
|
||||
const onClick = useGotoNode(node.id);
|
||||
|
||||
|
@ -21,11 +22,9 @@ const LabText: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
|||
<Paragraph lines={5} />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
className={styles.wrap}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Markdown className={styles.wrap} onClick={onClick}>
|
||||
{content}
|
||||
</Markdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,52 +1,35 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { MenuButton, MenuItemWithIcon } from '~/components/menu';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { IUser } from '~/types/auth';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
user: Partial<IUser>;
|
||||
onLogout: () => void;
|
||||
authOpenProfile: () => void;
|
||||
username: string;
|
||||
photo?: IFile;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, onLogout }) => {
|
||||
const onProfileOpen = useCallback(() => {
|
||||
authOpenProfile();
|
||||
}, [authOpenProfile]);
|
||||
|
||||
const onSettingsOpen = useCallback(() => {
|
||||
authOpenProfile();
|
||||
}, [authOpenProfile]);
|
||||
|
||||
const UserButton: FC<IProps> = ({ username, photo, onClick }) => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<button className={styles.wrap} onClick={onClick}>
|
||||
<Group horizontal className={styles.user_button}>
|
||||
<div className={styles.username}>{username}</div>
|
||||
|
||||
<MenuButton
|
||||
position="bottom"
|
||||
translucent={false}
|
||||
icon={
|
||||
<div
|
||||
className={styles.user_avatar}
|
||||
style={{ backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')` }}
|
||||
>
|
||||
{(!photo || !photo.id) && <Icon icon="profile" />}
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className={styles.user_avatar}
|
||||
style={{
|
||||
backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')`,
|
||||
}}
|
||||
>
|
||||
<MenuItemWithIcon onClick={onProfileOpen}>Профиль</MenuItemWithIcon>
|
||||
<MenuItemWithIcon onClick={onSettingsOpen}>Настройки</MenuItemWithIcon>
|
||||
<MenuItemWithIcon onClick={onLogout}>Выдох</MenuItemWithIcon>
|
||||
</MenuButton>
|
||||
{(!photo || !photo.id) && <Icon icon="profile" />}
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import React, { FC, useCallback } from 'react';
|
|||
import { Avatar } from '~/components/common/Avatar';
|
||||
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
||||
import { INodeUser } from '~/types';
|
||||
import { openUserProfile } from '~/utils/user';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
@ -12,8 +11,6 @@ interface Props {
|
|||
}
|
||||
|
||||
const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
||||
const onOpenProfile = useCallback(() => openUserProfile(user?.username), [user]);
|
||||
|
||||
const description = useUserDescription(user);
|
||||
|
||||
if (!user) {
|
||||
|
@ -23,7 +20,7 @@ const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
|||
const { fullname, username, photo } = user;
|
||||
|
||||
return (
|
||||
<div className={styles.block} onClick={onOpenProfile}>
|
||||
<div className={styles.block}>
|
||||
<Avatar username={username} url={photo?.url} className={styles.avatar} />
|
||||
|
||||
<div className={styles.info}>
|
||||
|
|
|
@ -30,16 +30,19 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
|
|||
onLock,
|
||||
onEdit,
|
||||
}) => {
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
if (isMobile) {
|
||||
if (isTablet) {
|
||||
return (
|
||||
<MenuButton
|
||||
icon={<Icon icon="dots-vertical" className={styles.icon} size={24} />}
|
||||
className={className}
|
||||
>
|
||||
{canStar && (
|
||||
<MenuItemWithIcon icon={isHeroic ? 'star_full' : 'star'} onClick={onStar}>
|
||||
<MenuItemWithIcon
|
||||
icon={isHeroic ? 'star_full' : 'star'}
|
||||
onClick={onStar}
|
||||
>
|
||||
{isHeroic ? 'Убрать с главной' : 'На главную'}
|
||||
</MenuItemWithIcon>
|
||||
)}
|
||||
|
@ -48,7 +51,10 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
|
|||
Редактировать
|
||||
</MenuItemWithIcon>
|
||||
|
||||
<MenuItemWithIcon icon={isLocked ? 'locked' : 'unlocked'} onClick={onLock}>
|
||||
<MenuItemWithIcon
|
||||
icon={isLocked ? 'locked' : 'unlocked'}
|
||||
onClick={onLock}
|
||||
>
|
||||
{isLocked ? 'Восстановить' : 'Удалить'}
|
||||
</MenuItemWithIcon>
|
||||
</MenuButton>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { VFC } from 'react';
|
||||
import React, { useCallback, useState, VFC } from 'react';
|
||||
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Markdown } from '~/components/containers/Markdown';
|
||||
|
@ -6,22 +6,56 @@ import { Padder } from '~/components/containers/Padder';
|
|||
import { NoteMenu } from '~/components/notes/NoteMenu';
|
||||
import { formatText, getPrettyDate } from '~/utils/dom';
|
||||
|
||||
import { NoteCreationForm } from '../NoteCreationForm';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface NoteCardProps {
|
||||
content: string;
|
||||
remove: () => Promise<void>;
|
||||
update: (text: string, callback?: () => void) => Promise<void>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const NoteCard: VFC<NoteCardProps> = ({ content, createdAt }) => (
|
||||
<Card className={styles.note}>
|
||||
<Padder>
|
||||
<NoteMenu onEdit={console.log} onDelete={console.log} />
|
||||
<Markdown className={styles.wrap} dangerouslySetInnerHTML={{ __html: formatText(content) }} />
|
||||
</Padder>
|
||||
const NoteCard: VFC<NoteCardProps> = ({
|
||||
content,
|
||||
createdAt,
|
||||
remove,
|
||||
update,
|
||||
}) => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
||||
</Card>
|
||||
);
|
||||
const toggleEditing = useCallback(() => setEditing(v => !v), []);
|
||||
const onUpdate = useCallback(
|
||||
(text: string, callback?: () => void) =>
|
||||
update(text, () => {
|
||||
setEditing(false);
|
||||
callback?.();
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={styles.note}>
|
||||
{editing ? (
|
||||
<NoteCreationForm
|
||||
text={content}
|
||||
onSubmit={onUpdate}
|
||||
onCancel={toggleEditing}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Padder>
|
||||
<NoteMenu onEdit={toggleEditing} onDelete={remove} />
|
||||
|
||||
<Markdown className={styles.wrap}>{formatText(content)}</Markdown>
|
||||
</Padder>
|
||||
|
||||
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export { NoteCard };
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
word-break: break-word;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
& > * {
|
||||
@include row_shadow;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
99
src/components/notes/NoteCreationForm/index.tsx
Normal file
99
src/components/notes/NoteCreationForm/index.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
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 { Button } from '~/components/input/Button';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { getErrorMessage } from '~/utils/errors/getErrorMessage';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface NoteCreationFormProps {
|
||||
text?: string;
|
||||
onSubmit: (text: string, callback: () => void) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const validationSchema = object({
|
||||
text: string().required('Напишите что-нибудь'),
|
||||
});
|
||||
|
||||
type Values = Asserts<typeof validationSchema>;
|
||||
|
||||
const NoteCreationForm: FC<NoteCreationFormProps> = ({
|
||||
text = '',
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const placeholder = useRandomPhrase('SIMPLE');
|
||||
|
||||
const submit = useCallback<FormikConfig<Values>['onSubmit']>(
|
||||
async (values, { resetForm, setSubmitting, setErrors }) => {
|
||||
try {
|
||||
await onSubmit(values.text, () => resetForm());
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
if (message) {
|
||||
setErrors({ text: message });
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorToast(error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[onSubmit],
|
||||
);
|
||||
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
touched,
|
||||
handleBlur,
|
||||
isSubmitting,
|
||||
} = useFormik<Values>({
|
||||
initialValues: { text },
|
||||
validationSchema,
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.row}>
|
||||
<Textarea
|
||||
handler={handleChange('text')}
|
||||
value={values.text}
|
||||
error={touched.text ? errors.text : undefined}
|
||||
onBlur={handleBlur('text')}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Group horizontal className={styles.row}>
|
||||
<Filler />
|
||||
|
||||
<Button size="mini" type="button" color="link" onClick={onCancel}>
|
||||
Отмена
|
||||
</Button>
|
||||
|
||||
<Button size="mini" type="submit" color="gray" loading={isSubmitting}>
|
||||
ОК
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export { NoteCreationForm };
|
11
src/components/notes/NoteCreationForm/styles.module.scss
Normal file
11
src/components/notes/NoteCreationForm/styles.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.card {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
@include row_shadow;
|
||||
|
||||
padding: $gap / 2;
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import { FC } from "react";
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Filler } from "~/components/containers/Filler";
|
||||
import { Group } from "~/components/containers/Group";
|
||||
import { Padder } from "~/components/containers/Padder";
|
||||
import { Button } from "~/components/input/Button";
|
||||
import { UserSettingsView } from "~/containers/settings/UserSettingsView";
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { UserSettingsView } from '~/containers/settings/UserSettingsView';
|
||||
import {
|
||||
SettingsProvider,
|
||||
useSettings,
|
||||
} from "~/utils/providers/SettingsProvider";
|
||||
} from '~/utils/providers/SettingsProvider';
|
||||
|
||||
const Form = ({ children }) => {
|
||||
const { handleSubmit } = useSettings();
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import React, { VFC } from 'react';
|
||||
import { VFC } from 'react';
|
||||
|
||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||
import { SettingsNotes } from '~/containers/settings/SettingsNotes';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface ProfileSidebarNotesProps {}
|
||||
|
||||
const ProfileSidebarNotes: VFC<ProfileSidebarNotesProps> = () => {
|
||||
const { closeAllTabs } = useStackContext();
|
||||
|
||||
return (
|
||||
<SidebarStackCard width={800} headerFeature="back" title="Заметки" onBackPress={closeAllTabs}>
|
||||
<div className={styles.scroller}>
|
||||
<SettingsNotes />
|
||||
</div>
|
||||
<SidebarStackCard
|
||||
width={480}
|
||||
headerFeature="back"
|
||||
title="Заметки"
|
||||
onBackPress={closeAllTabs}
|
||||
>
|
||||
<SettingsNotes />
|
||||
</SidebarStackCard>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React, { FC } from "react";
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { Filler } from "~/components/containers/Filler";
|
||||
import { Button } from "~/components/input/Button";
|
||||
import { ProfileSettings } from "~/components/profile/ProfileSettings";
|
||||
import { useStackContext } from "~/components/sidebar/SidebarStack";
|
||||
import { SidebarStackCard } from "~/components/sidebar/SidebarStackCard";
|
||||
import { UserSettingsView } from "~/containers/settings/UserSettingsView";
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ProfileSettings } from '~/components/profile/ProfileSettings';
|
||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||
import { UserSettingsView } from '~/containers/settings/UserSettingsView';
|
||||
import {
|
||||
SettingsProvider,
|
||||
useSettings,
|
||||
} from "~/utils/providers/SettingsProvider";
|
||||
} from '~/utils/providers/SettingsProvider';
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
import React, { createContext, FC, PropsWithChildren, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { isNil } from '~/utils/ramda';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface SidebarStackProps extends PropsWithChildren<{}> {
|
||||
initialTab?: number;
|
||||
tab?: number;
|
||||
onTabChange?: (index?: number) => void;
|
||||
}
|
||||
|
||||
interface SidebarStackContextValue {
|
||||
|
@ -38,12 +48,32 @@ const SidebarCards: FC = ({ children }) => {
|
|||
return <div className={styles.card}>{nonEmptyChildren[activeTab]}</div>;
|
||||
};
|
||||
|
||||
const SidebarStack = function({ children, initialTab }: SidebarStackProps) {
|
||||
const [activeTab, setActiveTab] = useState<number | undefined>(initialTab);
|
||||
const closeAllTabs = useCallback(() => setActiveTab(undefined), []);
|
||||
const SidebarStack = function({
|
||||
children,
|
||||
tab,
|
||||
onTabChange,
|
||||
}: SidebarStackProps) {
|
||||
const [activeTab, setActiveTab] = useState<number | undefined>(tab);
|
||||
|
||||
const closeAllTabs = useCallback(() => {
|
||||
setActiveTab(undefined);
|
||||
onTabChange?.(undefined);
|
||||
}, []);
|
||||
|
||||
const onChangeTab = useCallback(
|
||||
(index: number) => {
|
||||
onTabChange?.(index);
|
||||
setActiveTab(index);
|
||||
},
|
||||
[onTabChange],
|
||||
);
|
||||
|
||||
useEffect(() => setActiveTab(tab), [tab]);
|
||||
|
||||
return (
|
||||
<SidebarStackContext.Provider value={{ activeTab, setActiveTab, closeAllTabs }}>
|
||||
<SidebarStackContext.Provider
|
||||
value={{ activeTab, setActiveTab: onChangeTab, closeAllTabs }}
|
||||
>
|
||||
<div className={styles.stack}>{children}</div>
|
||||
</SidebarStackContext.Provider>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue