mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36: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
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,3 +7,5 @@
|
||||||
/.vscode
|
/.vscode
|
||||||
/.history
|
/.history
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/tsconfig.tsbuildinfo
|
||||||
|
.env.local
|
|
@ -94,7 +94,7 @@
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"lint-staged": "^12.1.6",
|
"lint-staged": "^12.1.6",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^9.0.0",
|
||||||
"prettier": "^1.18.2"
|
"prettier": "^2.7.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"./**/*.{js,jsx,ts,tsx}": [
|
"./**/*.{js,jsx,ts,tsx}": [
|
||||||
|
|
|
@ -1,8 +1,31 @@
|
||||||
import { ApiGetNotesRequest, ApiGetNotesResponse } from '~/api/notes/types';
|
import {
|
||||||
import { URLS } from '~/constants/urls';
|
ApiGetNotesRequest as ApiListNotesRequest,
|
||||||
import { api, cleanResult } from '~/utils/api';
|
ApiGetNotesResponse,
|
||||||
|
ApiCreateNoteRequest,
|
||||||
|
ApiUpdateNoteResponse,
|
||||||
|
ApiUpdateNoteRequest,
|
||||||
|
} from "~/api/notes/types";
|
||||||
|
import { URLS } from "~/constants/urls";
|
||||||
|
import { api, cleanResult } from "~/utils/api";
|
||||||
|
|
||||||
export const apiGetNotes = ({ limit, offset, search }: ApiGetNotesRequest) =>
|
export const apiListNotes = ({ limit, offset, search }: ApiListNotesRequest) =>
|
||||||
api
|
api
|
||||||
.get<ApiGetNotesResponse>(URLS.NOTES, { params: { limit, offset, search } })
|
.get<ApiGetNotesResponse>(URLS.NOTES, { params: { limit, offset, search } })
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
|
export const apiCreateNote = ({ text }: ApiCreateNoteRequest) =>
|
||||||
|
api
|
||||||
|
.post<ApiUpdateNoteResponse>(URLS.NOTES, {
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
.then(cleanResult);
|
||||||
|
|
||||||
|
export const apiDeleteNote = (id: number) =>
|
||||||
|
api.delete(URLS.NOTE(id)).then(cleanResult);
|
||||||
|
|
||||||
|
export const apiUpdateNote = ({ id, text }: ApiUpdateNoteRequest) =>
|
||||||
|
api
|
||||||
|
.put<ApiUpdateNoteResponse>(URLS.NOTE(id), {
|
||||||
|
content: text,
|
||||||
|
})
|
||||||
|
.then(cleanResult);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Note } from '~/types/notes';
|
import { Note } from "~/types/notes";
|
||||||
|
|
||||||
export interface ApiGetNotesRequest {
|
export interface ApiGetNotesRequest {
|
||||||
limit: number;
|
limit: number;
|
||||||
|
@ -10,3 +10,14 @@ export interface ApiGetNotesResponse {
|
||||||
list: Note[];
|
list: Note[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiCreateNoteRequest {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiUpdateNoteResponse extends Note {}
|
||||||
|
|
||||||
|
export interface ApiUpdateNoteRequest {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
|
@ -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 {}
|
interface LoginSceneProps {}
|
||||||
|
|
||||||
|
@ -17,31 +17,31 @@ interface Layer {
|
||||||
|
|
||||||
const layers: Layer[] = [
|
const layers: Layer[] = [
|
||||||
{
|
{
|
||||||
src: "/images/clouds__bg.svg",
|
src: '/images/clouds__bg.svg',
|
||||||
velocity: -0.3,
|
velocity: -0.3,
|
||||||
width: 3840,
|
width: 3840,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/clouds__cube.svg",
|
src: '/images/clouds__cube.svg',
|
||||||
velocity: -0.1,
|
velocity: -0.1,
|
||||||
width: 3840,
|
width: 3840,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/clouds__cloud.svg",
|
src: '/images/clouds__cloud.svg',
|
||||||
velocity: 0.2,
|
velocity: 0.2,
|
||||||
width: 3840,
|
width: 3840,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/clouds__dudes.svg",
|
src: '/images/clouds__dudes.svg',
|
||||||
velocity: 0.5,
|
velocity: 0.5,
|
||||||
width: 3840,
|
width: 3840,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/clouds__trash.svg",
|
src: '/images/clouds__trash.svg',
|
||||||
velocity: 0.8,
|
velocity: 0.8,
|
||||||
width: 3840,
|
width: 3840,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
|
@ -52,7 +52,7 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const imageRefs = useRef<Array<SVGImageElement | null>>([]);
|
const imageRefs = useRef<Array<SVGImageElement | null>>([]);
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
const domRect = useRef<DOMRect>();
|
const domRect = useRef<DOMRect>();
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
const onMouseMove = useCallback(
|
||||||
|
@ -84,11 +84,11 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = throttle(100, onMouseMove);
|
const listener = throttle(100, onMouseMove);
|
||||||
document.addEventListener("mousemove", listener);
|
document.addEventListener('mousemove', listener);
|
||||||
return () => document.removeEventListener("mousemove", listener);
|
return () => document.removeEventListener('mousemove', listener);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isMobile) {
|
if (isTablet) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,16 +103,16 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="fallbackGradient" x1={0} x2={0} y1={1} y2={0}>
|
<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
|
<stop
|
||||||
style={{ stopColor: "#fff6d5", stopOpacity: 1 }}
|
style={{ stopColor: '#fff6d5', stopOpacity: 1 }}
|
||||||
offset="0.34655526"
|
offset="0.34655526"
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
style={{ stopColor: "#afc6e9", stopOpacity: 1 }}
|
style={{ stopColor: '#afc6e9', stopOpacity: 1 }}
|
||||||
offset="0.765342"
|
offset="0.765342"
|
||||||
/>
|
/>
|
||||||
<stop style={{ stopColor: "#879fde", stopOpacity: 1 }} offset="1" />
|
<stop style={{ stopColor: '#879fde', stopOpacity: 1 }} offset="1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const Superpower: FC<IProps> = ({ children }) => {
|
const Superpower: FC<IProps> = observer(({ children }) => {
|
||||||
const { isTester } = useAuth();
|
const { isTester } = useAuth();
|
||||||
|
|
||||||
if (!isTester) return null;
|
if (!isTester) return null;
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export { Superpower };
|
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 classNames from 'classnames';
|
||||||
|
|
||||||
import styles from '~/styles/common/markdown.module.scss';
|
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 }) => (
|
const Markdown: VFC<IProps> = ({ className, children = '', ...props }) => (
|
||||||
<div className={classNames(styles.wrapper, className)} {...props} />
|
<div
|
||||||
|
className={classNames(styles.wrapper, className)}
|
||||||
|
{...props}
|
||||||
|
dangerouslySetInnerHTML={{ __html: formatText(children) }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export { Markdown };
|
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 {
|
interface ZoneProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: "danger" | "normal";
|
color?: 'danger' | 'normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Zone: FC<ZoneProps> = ({
|
const Zone: FC<ZoneProps> = ({
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
children,
|
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 && (
|
{!!title && (
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
|
|
|
@ -8,7 +8,7 @@ $pad_usual: mix(white, $content_bg, 10%);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -5px;
|
top: -$gap;
|
||||||
left: $radius;
|
left: $radius;
|
||||||
transform: translate(0, -100%);
|
transform: translate(0, -100%);
|
||||||
background: $pad_usual;
|
background: $pad_usual;
|
||||||
|
@ -25,7 +25,7 @@ $pad_usual: mix(white, $content_bg, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pad {
|
.pad {
|
||||||
padding: $gap * 1.5 $gap $gap;
|
padding: $gap;
|
||||||
box-shadow: inset $pad_usual 0 0 0 2px;
|
box-shadow: inset $pad_usual 0 0 0 2px;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -33,4 +33,8 @@ $pad_usual: mix(white, $content_bg, 10%);
|
||||||
&.danger {
|
&.danger {
|
||||||
box-shadow: inset $pad_danger 0 0 0 2px;
|
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 EditorButtons: FC = () => {
|
||||||
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Padder style={{ position: 'relative' }}>
|
<Padder style={{ position: 'relative' }}>
|
||||||
|
@ -23,14 +23,14 @@ const EditorButtons: FC = () => {
|
||||||
title="Название"
|
title="Название"
|
||||||
value={values.title}
|
value={values.title}
|
||||||
handler={handleChange('title')}
|
handler={handleChange('title')}
|
||||||
autoFocus={!isMobile}
|
autoFocus={!isTablet}
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</Filler>
|
</Filler>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={isMobile ? undefined : 'Сохранить'}
|
title={isTablet ? undefined : 'Сохранить'}
|
||||||
iconRight="check"
|
iconRight="check"
|
||||||
color={values.is_promoted ? 'primary' : 'lab'}
|
color={values.is_promoted ? 'primary' : 'lab'}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
|
|
@ -12,20 +12,20 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
const onMove = useCallback(
|
const onMove = useCallback(
|
||||||
(newFiles: IFile[]) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles(newFiles.filter(it => it));
|
setFiles(newFiles.filter(it => it));
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[setFiles, files],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(id: IFile['id']) => {
|
(id: IFile['id']) => {
|
||||||
setFiles(files.filter(file => file && file.id !== id));
|
setFiles(files.filter(file => file && file.id !== id));
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[setFiles, files],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,7 +34,7 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
onSortEnd={onMove}
|
onSortEnd={onMove}
|
||||||
items={files}
|
items={files}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
size={!isMobile ? 220 : (innerWidth - 60) / 2}
|
size={!isTablet ? 220 : (innerWidth - 60) / 2}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,10 +39,12 @@ const FlowCell: FC<Props> = ({
|
||||||
canEdit = false,
|
canEdit = false,
|
||||||
onChangeCellView,
|
onChangeCellView,
|
||||||
}) => {
|
}) => {
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
const withText =
|
const withText =
|
||||||
((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text;
|
((!!flow.display && flow.display !== 'single') || !image) &&
|
||||||
|
flow.show_description &&
|
||||||
|
!!text;
|
||||||
const {
|
const {
|
||||||
hasDescription,
|
hasDescription,
|
||||||
setViewHorizontal,
|
setViewHorizontal,
|
||||||
|
@ -51,14 +53,19 @@ const FlowCell: FC<Props> = ({
|
||||||
setViewSingle,
|
setViewSingle,
|
||||||
toggleViewDescription,
|
toggleViewDescription,
|
||||||
} = useFlowCellControls(id, text, flow, onChangeCellView);
|
} = useFlowCellControls(id, text, flow, onChangeCellView);
|
||||||
const { isActive: isMenuActive, activate, ref, deactivate } = useClickOutsideFocus();
|
const {
|
||||||
|
isActive: isMenuActive,
|
||||||
|
activate,
|
||||||
|
ref,
|
||||||
|
deactivate,
|
||||||
|
} = useClickOutsideFocus();
|
||||||
|
|
||||||
const shadeSize = useMemo(() => {
|
const shadeSize = useMemo(() => {
|
||||||
const min = isMobile ? 10 : 15;
|
const min = isTablet ? 10 : 15;
|
||||||
const max = isMobile ? 20 : 40;
|
const max = isTablet ? 20 : 40;
|
||||||
|
|
||||||
return withText ? min : max;
|
return withText ? min : max;
|
||||||
}, [withText, isMobile]);
|
}, [withText, isTablet]);
|
||||||
|
|
||||||
const shadeAngle = useMemo(() => {
|
const shadeAngle = useMemo(() => {
|
||||||
if (flow.display === 'vertical') {
|
if (flow.display === 'vertical') {
|
||||||
|
@ -73,7 +80,10 @@ const FlowCell: FC<Props> = ({
|
||||||
}, [flow.display]);
|
}, [flow.display]);
|
||||||
|
|
||||||
return (
|
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 && (
|
{canEdit && !isMenuActive && (
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
<MenuDots onClick={activate} />
|
<MenuDots onClick={activate} />
|
||||||
|
@ -98,7 +108,10 @@ const FlowCell: FC<Props> = ({
|
||||||
|
|
||||||
<Anchor className={styles.link} href={to}>
|
<Anchor className={styles.link} href={to}>
|
||||||
{withText && (
|
{withText && (
|
||||||
<FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
|
<FlowCellText
|
||||||
|
className={styles.text}
|
||||||
|
heading={<h4 className={styles.title}>{title}</h4>}
|
||||||
|
>
|
||||||
{text!}
|
{text!}
|
||||||
</FlowCellText>
|
</FlowCellText>
|
||||||
)}
|
)}
|
||||||
|
@ -113,7 +126,12 @@ const FlowCell: FC<Props> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!title && (
|
{!!title && (
|
||||||
<CellShade color={color} className={styles.shade} size={shadeSize} angle={shadeAngle} />
|
<CellShade
|
||||||
|
color={color}
|
||||||
|
className={styles.shade}
|
||||||
|
size={shadeSize}
|
||||||
|
angle={shadeAngle}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!withText && (
|
{!withText && (
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { DivProps } from '~/utils/types';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
|
||||||
interface Props extends DivProps {
|
interface Props extends DivProps {
|
||||||
children: string;
|
children: string;
|
||||||
heading: string | ReactElement;
|
heading: string | ReactElement;
|
||||||
|
@ -17,10 +16,7 @@ interface Props extends DivProps {
|
||||||
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => (
|
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => (
|
||||||
<div {...rest} className={classNames(styles.text, rest.className)}>
|
<div {...rest} className={classNames(styles.text, rest.className)}>
|
||||||
{heading && <div className={styles.heading}>{heading}</div>}
|
{heading && <div className={styles.heading}>{heading}</div>}
|
||||||
<Markdown
|
<Markdown className={styles.description}>{formatText(children)}</Markdown>
|
||||||
className={styles.description}
|
|
||||||
dangerouslySetInnerHTML={{ __html: formatText(children) }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -34,14 +34,17 @@ const lazy = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
const { push } = useNavigation();
|
const { push } = useNavigation();
|
||||||
|
|
||||||
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
|
const [controlledSwiper, setControlledSwiper] = useState<
|
||||||
|
SwiperClass | undefined
|
||||||
|
>(undefined);
|
||||||
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
const [currentIndex, setCurrentIndex] = useState(heroes.length);
|
||||||
const preset = useMemo(() => (isMobile ? ImagePresets.cover : ImagePresets.small_hero), [
|
const preset = useMemo(
|
||||||
isMobile,
|
() => (isTablet ? ImagePresets.cover : ImagePresets.small_hero),
|
||||||
]);
|
[isTablet],
|
||||||
|
);
|
||||||
|
|
||||||
const onNext = useCallback(() => {
|
const onNext = useCallback(() => {
|
||||||
controlledSwiper?.slideNext(1);
|
controlledSwiper?.slideNext(1);
|
||||||
|
@ -79,7 +82,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||||
(sw: SwiperClass) => {
|
(sw: SwiperClass) => {
|
||||||
push(URLS.NODE_URL(heroes[sw.realIndex]?.id));
|
push(URLS.NODE_URL(heroes[sw.realIndex]?.id));
|
||||||
},
|
},
|
||||||
[push, heroes]
|
[push, heroes],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!heroes.length) {
|
if (!heroes.length) {
|
||||||
|
|
|
@ -20,11 +20,9 @@ const LabDescription: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
||||||
<Paragraph />
|
<Paragraph />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Markdown
|
<Markdown className={styles.wrap} onClick={onClick}>
|
||||||
className={styles.wrap}
|
{formatText(node.description)}
|
||||||
dangerouslySetInnerHTML={{ __html: formatText(node.description) }}
|
</Markdown>
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { path } from '~/utils/ramda';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
const LabText: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
const LabText: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
||||||
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [
|
const content = useMemo(
|
||||||
node,
|
() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''),
|
||||||
]);
|
[node],
|
||||||
|
);
|
||||||
|
|
||||||
const onClick = useGotoNode(node.id);
|
const onClick = useGotoNode(node.id);
|
||||||
|
|
||||||
|
@ -21,11 +22,9 @@ const LabText: FC<INodeComponentProps> = ({ node, isLoading }) => {
|
||||||
<Paragraph lines={5} />
|
<Paragraph lines={5} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Markdown
|
<Markdown className={styles.wrap} onClick={onClick}>
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
{content}
|
||||||
className={styles.wrap}
|
</Markdown>
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,52 +1,35 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { MenuButton, MenuItemWithIcon } from '~/components/menu';
|
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { ImagePresets } from '~/constants/urls';
|
||||||
import { IUser } from '~/types/auth';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
user: Partial<IUser>;
|
username: string;
|
||||||
onLogout: () => void;
|
photo?: IFile;
|
||||||
authOpenProfile: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, onLogout }) => {
|
const UserButton: FC<IProps> = ({ username, photo, onClick }) => {
|
||||||
const onProfileOpen = useCallback(() => {
|
|
||||||
authOpenProfile();
|
|
||||||
}, [authOpenProfile]);
|
|
||||||
|
|
||||||
const onSettingsOpen = useCallback(() => {
|
|
||||||
authOpenProfile();
|
|
||||||
}, [authOpenProfile]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<button className={styles.wrap} onClick={onClick}>
|
||||||
<Group horizontal className={styles.user_button}>
|
<Group horizontal className={styles.user_button}>
|
||||||
<div className={styles.username}>{username}</div>
|
<div className={styles.username}>{username}</div>
|
||||||
|
|
||||||
<MenuButton
|
|
||||||
position="bottom"
|
|
||||||
translucent={false}
|
|
||||||
icon={
|
|
||||||
<div
|
<div
|
||||||
className={styles.user_avatar}
|
className={styles.user_avatar}
|
||||||
style={{ backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')` }}
|
style={{
|
||||||
|
backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{(!photo || !photo.id) && <Icon icon="profile" />}
|
{(!photo || !photo.id) && <Icon icon="profile" />}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItemWithIcon onClick={onProfileOpen}>Профиль</MenuItemWithIcon>
|
|
||||||
<MenuItemWithIcon onClick={onSettingsOpen}>Настройки</MenuItemWithIcon>
|
|
||||||
<MenuItemWithIcon onClick={onLogout}>Выдох</MenuItemWithIcon>
|
|
||||||
</MenuButton>
|
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { FC, useCallback } from 'react';
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
||||||
import { INodeUser } from '~/types';
|
import { INodeUser } from '~/types';
|
||||||
import { openUserProfile } from '~/utils/user';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
@ -12,8 +11,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
||||||
const onOpenProfile = useCallback(() => openUserProfile(user?.username), [user]);
|
|
||||||
|
|
||||||
const description = useUserDescription(user);
|
const description = useUserDescription(user);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -23,7 +20,7 @@ const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
||||||
const { fullname, username, photo } = user;
|
const { fullname, username, photo } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.block} onClick={onOpenProfile}>
|
<div className={styles.block}>
|
||||||
<Avatar username={username} url={photo?.url} className={styles.avatar} />
|
<Avatar username={username} url={photo?.url} className={styles.avatar} />
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
|
|
@ -30,16 +30,19 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
|
||||||
onLock,
|
onLock,
|
||||||
onEdit,
|
onEdit,
|
||||||
}) => {
|
}) => {
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isTablet) {
|
||||||
return (
|
return (
|
||||||
<MenuButton
|
<MenuButton
|
||||||
icon={<Icon icon="dots-vertical" className={styles.icon} size={24} />}
|
icon={<Icon icon="dots-vertical" className={styles.icon} size={24} />}
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
{canStar && (
|
{canStar && (
|
||||||
<MenuItemWithIcon icon={isHeroic ? 'star_full' : 'star'} onClick={onStar}>
|
<MenuItemWithIcon
|
||||||
|
icon={isHeroic ? 'star_full' : 'star'}
|
||||||
|
onClick={onStar}
|
||||||
|
>
|
||||||
{isHeroic ? 'Убрать с главной' : 'На главную'}
|
{isHeroic ? 'Убрать с главной' : 'На главную'}
|
||||||
</MenuItemWithIcon>
|
</MenuItemWithIcon>
|
||||||
)}
|
)}
|
||||||
|
@ -48,7 +51,10 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
|
||||||
Редактировать
|
Редактировать
|
||||||
</MenuItemWithIcon>
|
</MenuItemWithIcon>
|
||||||
|
|
||||||
<MenuItemWithIcon icon={isLocked ? 'locked' : 'unlocked'} onClick={onLock}>
|
<MenuItemWithIcon
|
||||||
|
icon={isLocked ? 'locked' : 'unlocked'}
|
||||||
|
onClick={onLock}
|
||||||
|
>
|
||||||
{isLocked ? 'Восстановить' : 'Удалить'}
|
{isLocked ? 'Восстановить' : 'Удалить'}
|
||||||
</MenuItemWithIcon>
|
</MenuItemWithIcon>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { VFC } from 'react';
|
import React, { useCallback, useState, VFC } from 'react';
|
||||||
|
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from '~/components/containers/Card';
|
||||||
import { Markdown } from '~/components/containers/Markdown';
|
import { Markdown } from '~/components/containers/Markdown';
|
||||||
|
@ -6,22 +6,56 @@ import { Padder } from '~/components/containers/Padder';
|
||||||
import { NoteMenu } from '~/components/notes/NoteMenu';
|
import { NoteMenu } from '~/components/notes/NoteMenu';
|
||||||
import { formatText, getPrettyDate } from '~/utils/dom';
|
import { formatText, getPrettyDate } from '~/utils/dom';
|
||||||
|
|
||||||
|
import { NoteCreationForm } from '../NoteCreationForm';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface NoteCardProps {
|
interface NoteCardProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
remove: () => Promise<void>;
|
||||||
|
update: (text: string, callback?: () => void) => Promise<void>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoteCard: VFC<NoteCardProps> = ({ content, createdAt }) => (
|
const NoteCard: VFC<NoteCardProps> = ({
|
||||||
|
content,
|
||||||
|
createdAt,
|
||||||
|
remove,
|
||||||
|
update,
|
||||||
|
}) => {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const toggleEditing = useCallback(() => setEditing(v => !v), []);
|
||||||
|
const onUpdate = useCallback(
|
||||||
|
(text: string, callback?: () => void) =>
|
||||||
|
update(text, () => {
|
||||||
|
setEditing(false);
|
||||||
|
callback?.();
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<Card className={styles.note}>
|
<Card className={styles.note}>
|
||||||
|
{editing ? (
|
||||||
|
<NoteCreationForm
|
||||||
|
text={content}
|
||||||
|
onSubmit={onUpdate}
|
||||||
|
onCancel={toggleEditing}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Padder>
|
<Padder>
|
||||||
<NoteMenu onEdit={console.log} onDelete={console.log} />
|
<NoteMenu onEdit={toggleEditing} onDelete={remove} />
|
||||||
<Markdown className={styles.wrap} dangerouslySetInnerHTML={{ __html: formatText(content) }} />
|
|
||||||
|
<Markdown className={styles.wrap}>{formatText(content)}</Markdown>
|
||||||
</Padder>
|
</Padder>
|
||||||
|
|
||||||
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { NoteCard };
|
export { NoteCard };
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > * {
|
|
||||||
@include row_shadow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.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 { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from "~/components/containers/Group";
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Padder } from "~/components/containers/Padder";
|
import { Padder } from '~/components/containers/Padder';
|
||||||
import { Button } from "~/components/input/Button";
|
import { Button } from '~/components/input/Button';
|
||||||
import { UserSettingsView } from "~/containers/settings/UserSettingsView";
|
import { UserSettingsView } from '~/containers/settings/UserSettingsView';
|
||||||
import {
|
import {
|
||||||
SettingsProvider,
|
SettingsProvider,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "~/utils/providers/SettingsProvider";
|
} from '~/utils/providers/SettingsProvider';
|
||||||
|
|
||||||
const Form = ({ children }) => {
|
const Form = ({ children }) => {
|
||||||
const { handleSubmit } = useSettings();
|
const { handleSubmit } = useSettings();
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
import React, { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
|
|
||||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||||
import { SettingsNotes } from '~/containers/settings/SettingsNotes';
|
import { SettingsNotes } from '~/containers/settings/SettingsNotes';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface ProfileSidebarNotesProps {}
|
interface ProfileSidebarNotesProps {}
|
||||||
|
|
||||||
const ProfileSidebarNotes: VFC<ProfileSidebarNotesProps> = () => {
|
const ProfileSidebarNotes: VFC<ProfileSidebarNotesProps> = () => {
|
||||||
const { closeAllTabs } = useStackContext();
|
const { closeAllTabs } = useStackContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarStackCard width={800} headerFeature="back" title="Заметки" onBackPress={closeAllTabs}>
|
<SidebarStackCard
|
||||||
<div className={styles.scroller}>
|
width={480}
|
||||||
|
headerFeature="back"
|
||||||
|
title="Заметки"
|
||||||
|
onBackPress={closeAllTabs}
|
||||||
|
>
|
||||||
<SettingsNotes />
|
<SettingsNotes />
|
||||||
</div>
|
|
||||||
</SidebarStackCard>
|
</SidebarStackCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Filler } from "~/components/containers/Filler";
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Button } from "~/components/input/Button";
|
import { Button } from '~/components/input/Button';
|
||||||
import { ProfileSettings } from "~/components/profile/ProfileSettings";
|
import { ProfileSettings } from '~/components/profile/ProfileSettings';
|
||||||
import { useStackContext } from "~/components/sidebar/SidebarStack";
|
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||||
import { SidebarStackCard } from "~/components/sidebar/SidebarStackCard";
|
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||||
import { UserSettingsView } from "~/containers/settings/UserSettingsView";
|
import { UserSettingsView } from '~/containers/settings/UserSettingsView';
|
||||||
import {
|
import {
|
||||||
SettingsProvider,
|
SettingsProvider,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from "~/utils/providers/SettingsProvider";
|
} from '~/utils/providers/SettingsProvider';
|
||||||
|
|
||||||
import styles from "./styles.module.scss";
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface IProps {}
|
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 { isNil } from '~/utils/ramda';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface SidebarStackProps extends PropsWithChildren<{}> {
|
interface SidebarStackProps extends PropsWithChildren<{}> {
|
||||||
initialTab?: number;
|
tab?: number;
|
||||||
|
onTabChange?: (index?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarStackContextValue {
|
interface SidebarStackContextValue {
|
||||||
|
@ -38,12 +48,32 @@ const SidebarCards: FC = ({ children }) => {
|
||||||
return <div className={styles.card}>{nonEmptyChildren[activeTab]}</div>;
|
return <div className={styles.card}>{nonEmptyChildren[activeTab]}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SidebarStack = function({ children, initialTab }: SidebarStackProps) {
|
const SidebarStack = function({
|
||||||
const [activeTab, setActiveTab] = useState<number | undefined>(initialTab);
|
children,
|
||||||
const closeAllTabs = useCallback(() => setActiveTab(undefined), []);
|
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 (
|
return (
|
||||||
<SidebarStackContext.Provider value={{ activeTab, setActiveTab, closeAllTabs }}>
|
<SidebarStackContext.Provider
|
||||||
|
value={{ activeTab, setActiveTab: onChangeTab, closeAllTabs }}
|
||||||
|
>
|
||||||
<div className={styles.stack}>{children}</div>
|
<div className={styles.stack}>{children}</div>
|
||||||
</SidebarStackContext.Provider>
|
</SidebarStackContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const ERRORS = {
|
||||||
MESSAGE_NOT_FOUND: 'MessageNotFound',
|
MESSAGE_NOT_FOUND: 'MessageNotFound',
|
||||||
COMMENT_TOO_LONG: 'CommentTooLong',
|
COMMENT_TOO_LONG: 'CommentTooLong',
|
||||||
NETWORK_ERROR: 'Network Error',
|
NETWORK_ERROR: 'Network Error',
|
||||||
|
NOTE_NOT_FOUND: 'NoteNotFound',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERROR_LITERAL = {
|
export const ERROR_LITERAL = {
|
||||||
|
@ -74,9 +75,12 @@ export const ERROR_LITERAL = {
|
||||||
[ERRORS.INCORRECT_NODE_TYPE]: 'Ты пытаешься отправить пост неизвестного типа',
|
[ERRORS.INCORRECT_NODE_TYPE]: 'Ты пытаешься отправить пост неизвестного типа',
|
||||||
[ERRORS.UNEXPECTED_BEHAVIOR]: 'Что-то пошло не так. Напишите об этом Борису',
|
[ERRORS.UNEXPECTED_BEHAVIOR]: 'Что-то пошло не так. Напишите об этом Борису',
|
||||||
[ERRORS.FILES_IS_TOO_BIG]: 'Файл слишком большой',
|
[ERRORS.FILES_IS_TOO_BIG]: 'Файл слишком большой',
|
||||||
[ERRORS.OAUTH_CODE_IS_EMPTY]: 'Мы не смогли получить код от социальной сети. Попробуй ещё раз.',
|
[ERRORS.OAUTH_CODE_IS_EMPTY]:
|
||||||
[ERRORS.OAUTH_UNKNOWN_PROVIDER]: 'Ты пытаешься войти с помощью неизвестной социальной сети',
|
'Мы не смогли получить код от социальной сети. Попробуй ещё раз.',
|
||||||
[ERRORS.OAUTH_INVALID_DATA]: 'Социальная сеть вернула какую-то дичь. Попробуй ещё раз.',
|
[ERRORS.OAUTH_UNKNOWN_PROVIDER]:
|
||||||
|
'Ты пытаешься войти с помощью неизвестной социальной сети',
|
||||||
|
[ERRORS.OAUTH_INVALID_DATA]:
|
||||||
|
'Социальная сеть вернула какую-то дичь. Попробуй ещё раз.',
|
||||||
[ERRORS.USERNAME_IS_SHORT]: 'Хотя бы 2 символа',
|
[ERRORS.USERNAME_IS_SHORT]: 'Хотя бы 2 символа',
|
||||||
[ERRORS.USERNAME_CONTAINS_INVALID_CHARS]: 'Буквы, цифры и подчёркивание',
|
[ERRORS.USERNAME_CONTAINS_INVALID_CHARS]: 'Буквы, цифры и подчёркивание',
|
||||||
[ERRORS.PASSWORD_IS_SHORT]: 'Хотя бы 6 символов',
|
[ERRORS.PASSWORD_IS_SHORT]: 'Хотя бы 6 символов',
|
||||||
|
@ -91,4 +95,5 @@ export const ERROR_LITERAL = {
|
||||||
[ERRORS.MESSAGE_NOT_FOUND]: 'Сообщение не найдено',
|
[ERRORS.MESSAGE_NOT_FOUND]: 'Сообщение не найдено',
|
||||||
[ERRORS.COMMENT_TOO_LONG]: 'Комментарий слишком длинный',
|
[ERRORS.COMMENT_TOO_LONG]: 'Комментарий слишком длинный',
|
||||||
[ERRORS.NETWORK_ERROR]: 'Подключение не удалось',
|
[ERRORS.NETWORK_ERROR]: 'Подключение не удалось',
|
||||||
|
[ERRORS.NOTE_NOT_FOUND]: 'Заметка не найдена',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export enum EventMessageType {
|
export enum EventMessageType {
|
||||||
OpenProfile = 'open_profile',
|
|
||||||
OAuthLogin = 'oauth_login',
|
OAuthLogin = 'oauth_login',
|
||||||
OAuthProcessed = 'oauth_processed',
|
OAuthProcessed = 'oauth_processed',
|
||||||
OAuthError = 'oauth_error',
|
OAuthError = 'oauth_error',
|
||||||
|
|
|
@ -4,18 +4,15 @@ import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
|
||||||
import { LoginDialog } from '~/containers/dialogs/LoginDialog';
|
import { LoginDialog } from '~/containers/dialogs/LoginDialog';
|
||||||
import { LoginSocialRegisterDialog } from '~/containers/dialogs/LoginSocialRegisterDialog';
|
import { LoginSocialRegisterDialog } from '~/containers/dialogs/LoginSocialRegisterDialog';
|
||||||
import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe';
|
import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe';
|
||||||
import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
|
|
||||||
import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog';
|
import { RestorePasswordDialog } from '~/containers/dialogs/RestorePasswordDialog';
|
||||||
import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
|
import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
|
||||||
import { TestDialog } from '~/containers/dialogs/TestDialog';
|
import { TestDialog } from '~/containers/dialogs/TestDialog';
|
||||||
import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar';
|
|
||||||
import { TagSidebar } from '~/containers/sidebars/TagSidebar';
|
import { TagSidebar } from '~/containers/sidebars/TagSidebar';
|
||||||
|
|
||||||
export enum Dialog {
|
export enum Dialog {
|
||||||
Login = 'Login',
|
Login = 'Login',
|
||||||
LoginSocialRegister = 'LoginSocialRegister',
|
LoginSocialRegister = 'LoginSocialRegister',
|
||||||
Loading = 'Loading',
|
Loading = 'Loading',
|
||||||
Profile = 'Profile',
|
|
||||||
RestoreRequest = 'RestoreRequest',
|
RestoreRequest = 'RestoreRequest',
|
||||||
RestorePassword = 'RestorePassword',
|
RestorePassword = 'RestorePassword',
|
||||||
Test = 'Test',
|
Test = 'Test',
|
||||||
|
@ -23,7 +20,6 @@ export enum Dialog {
|
||||||
CreateNode = 'CreateNode',
|
CreateNode = 'CreateNode',
|
||||||
EditNode = 'EditNode',
|
EditNode = 'EditNode',
|
||||||
TagSidebar = 'TagNodes',
|
TagSidebar = 'TagNodes',
|
||||||
ProfileSidebar = 'ProfileSidebar',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DIALOG_CONTENT = {
|
export const DIALOG_CONTENT = {
|
||||||
|
@ -31,12 +27,10 @@ export const DIALOG_CONTENT = {
|
||||||
[Dialog.LoginSocialRegister]: LoginSocialRegisterDialog,
|
[Dialog.LoginSocialRegister]: LoginSocialRegisterDialog,
|
||||||
[Dialog.Loading]: LoadingDialog,
|
[Dialog.Loading]: LoadingDialog,
|
||||||
[Dialog.Test]: TestDialog,
|
[Dialog.Test]: TestDialog,
|
||||||
[Dialog.Profile]: ProfileDialog,
|
|
||||||
[Dialog.RestoreRequest]: RestoreRequestDialog,
|
[Dialog.RestoreRequest]: RestoreRequestDialog,
|
||||||
[Dialog.RestorePassword]: RestorePasswordDialog,
|
[Dialog.RestorePassword]: RestorePasswordDialog,
|
||||||
[Dialog.Photoswipe]: PhotoSwipe,
|
[Dialog.Photoswipe]: PhotoSwipe,
|
||||||
[Dialog.CreateNode]: EditorCreateDialog,
|
[Dialog.CreateNode]: EditorCreateDialog,
|
||||||
[Dialog.EditNode]: EditorEditDialog,
|
[Dialog.EditNode]: EditorEditDialog,
|
||||||
[Dialog.TagSidebar]: TagSidebar,
|
[Dialog.TagSidebar]: TagSidebar,
|
||||||
[Dialog.ProfileSidebar]: ProfileSidebar,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
9
src/constants/sidebar/components.ts
Normal file
9
src/constants/sidebar/components.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ProfileSidebar } from "~/containers/sidebars/ProfileSidebar";
|
||||||
|
|
||||||
|
import { SidebarName } from "./index";
|
||||||
|
|
||||||
|
export const sidebarComponents = {
|
||||||
|
[SidebarName.Settings]: ProfileSidebar,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SidebarComponents = typeof sidebarComponents;
|
|
@ -1,11 +1,5 @@
|
||||||
import { FC, ReactNode } from "react";
|
|
||||||
|
|
||||||
import { ProfileSidebar } from "~/containers/sidebars/ProfileSidebar";
|
import { ProfileSidebar } from "~/containers/sidebars/ProfileSidebar";
|
||||||
|
|
||||||
export enum SidebarName {
|
export enum SidebarName {
|
||||||
Settings = 'settings'
|
Settings = "settings",
|
||||||
}
|
|
||||||
|
|
||||||
export const sidebarComponents = {
|
|
||||||
[SidebarName.Settings]: ProfileSidebar
|
|
||||||
}
|
}
|
|
@ -1,48 +1,49 @@
|
||||||
import { FlowDisplayVariant, INode } from '~/types';
|
import { FlowDisplayVariant, INode } from "~/types";
|
||||||
|
|
||||||
export const URLS = {
|
export const URLS = {
|
||||||
BASE: '/',
|
BASE: "/",
|
||||||
LAB: '/lab',
|
LAB: "/lab",
|
||||||
BORIS: '/boris',
|
BORIS: "/boris",
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: '/auth/login',
|
LOGIN: "/auth/login",
|
||||||
},
|
},
|
||||||
EXAMPLES: {
|
EXAMPLES: {
|
||||||
EDITOR: '/examples/edit',
|
EDITOR: "/examples/edit",
|
||||||
IMAGE: '/examples/image',
|
IMAGE: "/examples/image",
|
||||||
},
|
},
|
||||||
ERRORS: {
|
ERRORS: {
|
||||||
NOT_FOUND: '/lost',
|
NOT_FOUND: "/lost",
|
||||||
BACKEND_DOWN: '/oopsie',
|
BACKEND_DOWN: "/oopsie",
|
||||||
},
|
},
|
||||||
NODE_URL: (id: INode['id'] | string) => `/post${id}`,
|
NODE_URL: (id: INode["id"] | string) => `/post${id}`,
|
||||||
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
PROFILE_PAGE: (username: string) => `/profile/${username}`,
|
||||||
SETTINGS: {
|
SETTINGS: {
|
||||||
BASE: '/settings',
|
BASE: "/settings",
|
||||||
NOTES: '/settings/notes',
|
NOTES: "/settings/notes",
|
||||||
TRASH: '/settings/trash',
|
TRASH: "/settings/trash",
|
||||||
},
|
},
|
||||||
NOTES: '/notes/',
|
NOTES: "/notes/",
|
||||||
|
NOTE: (id: number) => `/notes/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImagePresets = {
|
export const ImagePresets = {
|
||||||
'1600': '1600',
|
"1600": "1600",
|
||||||
'600': '600',
|
"600": "600",
|
||||||
'300': '300',
|
"300": "300",
|
||||||
cover: 'cover',
|
cover: "cover",
|
||||||
small_hero: 'small_hero',
|
small_hero: "small_hero",
|
||||||
avatar: 'avatar',
|
avatar: "avatar",
|
||||||
flow_square: 'flow_square',
|
flow_square: "flow_square",
|
||||||
flow_vertical: 'flow_vertical',
|
flow_vertical: "flow_vertical",
|
||||||
flow_horizontal: 'flow_horizontal',
|
flow_horizontal: "flow_horizontal",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const flowDisplayToPreset: Record<
|
export const flowDisplayToPreset: Record<
|
||||||
FlowDisplayVariant,
|
FlowDisplayVariant,
|
||||||
typeof ImagePresets[keyof typeof ImagePresets]
|
typeof ImagePresets[keyof typeof ImagePresets]
|
||||||
> = {
|
> = {
|
||||||
single: 'flow_square',
|
single: "flow_square",
|
||||||
quadro: 'flow_square',
|
quadro: "flow_square",
|
||||||
vertical: 'flow_vertical',
|
vertical: "flow_vertical",
|
||||||
horizontal: 'flow_horizontal',
|
horizontal: "flow_horizontal",
|
||||||
};
|
};
|
||||||
|
|
22
src/containers/auth/SuperPowersToggle/index.tsx
Normal file
22
src/containers/auth/SuperPowersToggle/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||||
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
|
import { useSuperPowers } from '~/hooks/auth/useSuperPowers';
|
||||||
|
|
||||||
|
interface SuperPowersToggleProps {}
|
||||||
|
|
||||||
|
const SuperPowersToggle: FC<SuperPowersToggleProps> = observer(() => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
const { isTester, setIsTester } = useSuperPowers();
|
||||||
|
|
||||||
|
if (!isUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BorisSuperpowers active={isTester} onChange={setIsTester} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { SuperPowersToggle };
|
|
@ -1,24 +1,22 @@
|
||||||
import React, { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { BorisContacts } from '~/components/boris/BorisContacts';
|
import { BorisContacts } from '~/components/boris/BorisContacts';
|
||||||
import { BorisStats } from '~/components/boris/BorisStats';
|
import { BorisStats } from '~/components/boris/BorisStats';
|
||||||
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||||
import styles from '~/layouts/BorisLayout/styles.module.scss';
|
import styles from '~/layouts/BorisLayout/styles.module.scss';
|
||||||
import { BorisUsageStats } from '~/types/boris';
|
import { BorisUsageStats } from '~/types/boris';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isUser: boolean;
|
isUser: boolean;
|
||||||
isTester: boolean;
|
|
||||||
stats: BorisUsageStats;
|
stats: BorisUsageStats;
|
||||||
setBetaTester: (val: boolean) => void;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading, isTester, setBetaTester }) => (
|
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading }) => (
|
||||||
<Group className={styles.stats__container}>
|
<Group className={styles.stats__container}>
|
||||||
<div className={styles.super_powers}>
|
<div className={styles.super_powers}>
|
||||||
{isUser && <BorisSuperpowers active={isTester} onChange={setBetaTester} />}
|
<SuperPowersToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BorisContacts />
|
<BorisContacts />
|
|
@ -1,39 +1,18 @@
|
||||||
import { FC, useCallback } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Markdown } from '~/components/containers/Markdown';
|
||||||
import { SidebarName } from '~/constants/sidebar';
|
|
||||||
import { URLS } from '~/constants/urls';
|
|
||||||
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
export interface BorisSuperpowersProps {}
|
export interface BorisSuperpowersProps {}
|
||||||
|
|
||||||
const BorisSuperpowers: FC<BorisSuperpowersProps> = () => {
|
const BorisSuperpowers: FC<BorisSuperpowersProps> = () => {
|
||||||
const { open } = useSidebar();
|
|
||||||
const openProfileSidebar = useCallback(() => {
|
|
||||||
open(SidebarName.Settings);
|
|
||||||
}, [open]);
|
|
||||||
const { push } = useRouter();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
<h2>Штучи, находящиеся в разработке</h2>
|
<h2>Штучки, находящиеся в разработке</h2>
|
||||||
|
|
||||||
<div className={styles.grid}>
|
<Markdown>
|
||||||
<Button size="mini" onClick={() => openProfileSidebar()}>
|
{`> На данный момент в разработке нет вещей, которые можно показать.\n\n// Приходите завтра`}
|
||||||
Открыть
|
</Markdown>
|
||||||
</Button>
|
|
||||||
<div className={styles.label}>Профиль в сайдбаре</div>
|
|
||||||
|
|
||||||
<Button size="mini" onClick={() => push(URLS.SETTINGS.BASE)}>
|
|
||||||
Открыть
|
|
||||||
</Button>
|
|
||||||
<div className={styles.label}>Профиль на отдельной странице</div>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import React, { FC, useCallback } from "react";
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { LoginDialogButtons } from "~/components/auth/login/LoginDialogButtons";
|
import { LoginDialogButtons } from '~/components/auth/login/LoginDialogButtons';
|
||||||
import { LoginScene } from "~/components/auth/login/LoginScene";
|
import { LoginScene } from '~/components/auth/login/LoginScene';
|
||||||
import { Group } from "~/components/containers/Group";
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Padder } from "~/components/containers/Padder";
|
import { Padder } from '~/components/containers/Padder';
|
||||||
import { BetterScrollDialog } from "~/components/dialogs/BetterScrollDialog";
|
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
|
||||||
import { DialogTitle } from "~/components/dialogs/DialogTitle";
|
import { DialogTitle } from '~/components/dialogs/DialogTitle';
|
||||||
import { Button } from "~/components/input/Button";
|
import { Button } from '~/components/input/Button';
|
||||||
import { InputText } from "~/components/input/InputText";
|
import { InputText } from '~/components/input/InputText';
|
||||||
import { Dialog } from "~/constants/modal";
|
import { Dialog } from '~/constants/modal';
|
||||||
import { useCloseOnEscape } from "~/hooks";
|
import { useCloseOnEscape } from '~/hooks';
|
||||||
import { useAuth } from "~/hooks/auth/useAuth";
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import { useLoginForm } from "~/hooks/auth/useLoginForm";
|
import { useLoginForm } from '~/hooks/auth/useLoginForm';
|
||||||
import { useOAuth } from "~/hooks/auth/useOAuth";
|
import { useOAuth } from '~/hooks/auth/useOAuth';
|
||||||
import { useShowModal } from "~/hooks/modal/useShowModal";
|
import { useShowModal } from '~/hooks/modal/useShowModal';
|
||||||
import { DialogComponentProps } from "~/types/modal";
|
import { DialogComponentProps } from '~/types/modal';
|
||||||
|
|
||||||
import styles from "./styles.module.scss";
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
type LoginDialogProps = DialogComponentProps & {};
|
type LoginDialogProps = DialogComponentProps & {};
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ const LoginDialog: FC<LoginDialogProps> = ({ onRequestClose }) => {
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
title="Логин"
|
title="Логин"
|
||||||
handler={handleChange("username")}
|
handler={handleChange('username')}
|
||||||
value={values.username}
|
value={values.username}
|
||||||
error={errors.username}
|
error={errors.username}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
@ -63,7 +63,7 @@ const LoginDialog: FC<LoginDialogProps> = ({ onRequestClose }) => {
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
title="Пароль"
|
title="Пароль"
|
||||||
handler={handleChange("password")}
|
handler={handleChange('password')}
|
||||||
value={values.password}
|
value={values.password}
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
type="password"
|
type="password"
|
||||||
|
|
|
@ -22,7 +22,7 @@ export interface PhotoSwipeProps extends DialogComponentProps {
|
||||||
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
let ref = useRef<HTMLDivElement>(null);
|
let ref = useRef<HTMLDivElement>(null);
|
||||||
const { hideModal } = useModal();
|
const { hideModal } = useModal();
|
||||||
const { isMobile } = useWindowSize();
|
const { isTablet } = useWindowSize();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new Promise(async resolve => {
|
new Promise(async resolve => {
|
||||||
|
@ -34,7 +34,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
resolveImage({
|
resolveImage({
|
||||||
src: getURL(image, isMobile ? ImagePresets[900] : ImagePresets[1600]),
|
src: getURL(
|
||||||
|
image,
|
||||||
|
isTablet ? ImagePresets[900] : ImagePresets[1600],
|
||||||
|
),
|
||||||
h: img.naturalHeight,
|
h: img.naturalHeight,
|
||||||
w: img.naturalWidth,
|
w: img.naturalWidth,
|
||||||
});
|
});
|
||||||
|
@ -45,8 +48,8 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = getURL(image, ImagePresets[1600]);
|
img.src = getURL(image, ImagePresets[1600]);
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve(images);
|
resolve(images);
|
||||||
|
@ -61,10 +64,16 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
ps.listen('destroy', hideModal);
|
ps.listen('destroy', hideModal);
|
||||||
ps.listen('close', hideModal);
|
ps.listen('close', hideModal);
|
||||||
});
|
});
|
||||||
}, [hideModal, items, index, isMobile]);
|
}, [hideModal, items, index, isTablet]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pswp" tabIndex={-1} role="dialog" aria-hidden="true" ref={ref}>
|
<div
|
||||||
|
className="pswp"
|
||||||
|
tabIndex={-1}
|
||||||
|
role="dialog"
|
||||||
|
aria-hidden="true"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div className={classNames('pswp__bg', styles.bg)} />
|
<div className={classNames('pswp__bg', styles.bg)} />
|
||||||
<div className={classNames('pswp__scroll-wrap', styles.wrap)}>
|
<div className={classNames('pswp__scroll-wrap', styles.wrap)}>
|
||||||
<div className="pswp__container">
|
<div className="pswp__container">
|
||||||
|
@ -76,7 +85,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
<div className="pswp__ui pswp__ui--hidden">
|
<div className="pswp__ui pswp__ui--hidden">
|
||||||
<div className={classNames('pswp__top-bar', styles.bar)}>
|
<div className={classNames('pswp__top-bar', styles.bar)}>
|
||||||
<div className="pswp__counter" />
|
<div className="pswp__counter" />
|
||||||
<button className="pswp__button pswp__button--close" title="Close (Esc)" />
|
<button
|
||||||
|
className="pswp__button pswp__button--close"
|
||||||
|
title="Close (Esc)"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="pswp__preloader">
|
<div className="pswp__preloader">
|
||||||
<div className="pswp__preloader__icn">
|
<div className="pswp__preloader__icn">
|
||||||
|
@ -96,7 +108,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||||
title="Previous (arrow left)"
|
title="Previous (arrow left)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button className="pswp__button pswp__button--arrow--right" title="Next (arrow right)" />
|
<button
|
||||||
|
className="pswp__button pswp__button--arrow--right"
|
||||||
|
title="Next (arrow right)"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="pswp__caption">
|
<div className="pswp__caption">
|
||||||
<div className="pswp__caption__center" />
|
<div className="pswp__caption__center" />
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
|
||||||
import { Tabs } from '~/components/dialogs/Tabs';
|
|
||||||
import { ProfileDescription } from '~/components/profile/ProfileDescription';
|
|
||||||
import { ProfileSettings } from '~/components/profile/ProfileSettings';
|
|
||||||
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
|
|
||||||
import { ProfileInfo } from '~/containers/profile/ProfileInfo';
|
|
||||||
import { ProfileMessages } from '~/containers/profile/ProfileMessages';
|
|
||||||
import { useUser } from '~/hooks/auth/useUser';
|
|
||||||
import { useGetProfile } from '~/hooks/profile/useGetProfile';
|
|
||||||
import { DialogComponentProps } from '~/types/modal';
|
|
||||||
import { ProfileProvider } from '~/utils/providers/ProfileProvider';
|
|
||||||
|
|
||||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
|
||||||
|
|
||||||
export interface ProfileDialogProps extends DialogComponentProps {
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProfileDialog: FC<ProfileDialogProps> = ({ username, onRequestClose }) => {
|
|
||||||
const { isLoading, profile } = useGetProfile(username);
|
|
||||||
const {
|
|
||||||
user: { id },
|
|
||||||
} = useUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProfileProvider username={username}>
|
|
||||||
<Tabs>
|
|
||||||
<BetterScrollDialog
|
|
||||||
header={<ProfileInfo isOwn={profile.id === id} isLoading={isLoading} />}
|
|
||||||
backdrop={<CoverBackdrop cover={profile.cover} />}
|
|
||||||
onClose={onRequestClose}
|
|
||||||
>
|
|
||||||
<Tabs.Content>
|
|
||||||
<ProfileDescription />
|
|
||||||
<ProfileMessages />
|
|
||||||
<ProfileSettings />
|
|
||||||
<ProfileAccounts />
|
|
||||||
</Tabs.Content>
|
|
||||||
</BetterScrollDialog>
|
|
||||||
</Tabs>
|
|
||||||
</ProfileProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ProfileDialog };
|
|
|
@ -1,5 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.messages {
|
|
||||||
padding: $gap;
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@ import { Button } from '~/components/input/Button';
|
||||||
import { Logo } from '~/components/main/Logo';
|
import { Logo } from '~/components/main/Logo';
|
||||||
import { UserButton } from '~/components/main/UserButton';
|
import { UserButton } from '~/components/main/UserButton';
|
||||||
import { Dialog } from '~/constants/modal';
|
import { Dialog } from '~/constants/modal';
|
||||||
|
import { SidebarName } from '~/constants/sidebar';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import { useScrollTop } from '~/hooks/dom/useScrollTop';
|
import { useScrollTop } from '~/hooks/dom/useScrollTop';
|
||||||
|
@ -18,6 +19,7 @@ import { useFlow } from '~/hooks/flow/useFlow';
|
||||||
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||||
import { useModal } from '~/hooks/modal/useModal';
|
import { useModal } from '~/hooks/modal/useModal';
|
||||||
import { useUpdates } from '~/hooks/updates/useUpdates';
|
import { useUpdates } from '~/hooks/updates/useUpdates';
|
||||||
|
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
@ -27,15 +29,15 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
const labStats = useGetLabStats();
|
const labStats = useGetLabStats();
|
||||||
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const { logout } = useAuth();
|
|
||||||
const { showModal } = useModal();
|
const { showModal } = useModal();
|
||||||
const { isUser, user } = useAuth();
|
const { isUser, user } = useAuth();
|
||||||
const { updates: flowUpdates } = useFlow();
|
const { updates: flowUpdates } = useFlow();
|
||||||
const { borisCommentedAt } = useUpdates();
|
const { borisCommentedAt } = useUpdates();
|
||||||
|
const { open } = useSidebar();
|
||||||
|
|
||||||
const openProfile = useCallback(() => {
|
const openProfileSidebar = useCallback(() => {
|
||||||
showModal(Dialog.Profile, { username: user.username });
|
open(SidebarName.Settings, {});
|
||||||
}, [user.username, showModal]);
|
}, [open]);
|
||||||
|
|
||||||
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
|
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
|
||||||
|
|
||||||
|
@ -47,10 +49,12 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
borisCommentedAt &&
|
borisCommentedAt &&
|
||||||
(!user.last_seen_boris ||
|
(!user.last_seen_boris ||
|
||||||
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
|
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
|
||||||
[borisCommentedAt, isUser, user.last_seen_boris]
|
[borisCommentedAt, isUser, user.last_seen_boris],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [labStats.updates]);
|
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [
|
||||||
|
labStats.updates,
|
||||||
|
]);
|
||||||
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
||||||
|
|
||||||
// Needed for SSR
|
// Needed for SSR
|
||||||
|
@ -59,7 +63,9 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
}, [top]);
|
}, [top]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}>
|
<header
|
||||||
|
className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}
|
||||||
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.logo_wrapper}>
|
<div className={styles.logo_wrapper}>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
@ -98,10 +104,21 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{isUser && <UserButton user={user} onLogout={logout} authOpenProfile={openProfile} />}
|
{isUser && (
|
||||||
|
<UserButton
|
||||||
|
username={user.username}
|
||||||
|
photo={user.photo}
|
||||||
|
onClick={openProfileSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<Button className={styles.user_button} onClick={onLogin} round color="secondary">
|
<Button
|
||||||
|
className={styles.user_button}
|
||||||
|
onClick={onLogin}
|
||||||
|
round
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
ВДОХ
|
ВДОХ
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -26,17 +26,19 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile, isLoading }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.region}>
|
<div className={styles.region}>
|
||||||
<div className={styles.name}>{isLoading ? <Placeholder /> : profile?.fullname}</div>`
|
<div className={styles.name}>
|
||||||
|
{isLoading ? <Placeholder /> : profile?.fullname}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
<div className={styles.username}>
|
<div className={styles.username}>
|
||||||
{isLoading ? <Placeholder /> : `~${profile?.username}`}
|
{isLoading ? <Placeholder /> : `~${profile?.username}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!profile?.description && (
|
{!!profile?.description && (
|
||||||
<Markdown
|
<Markdown className={styles.description}>
|
||||||
className={styles.description}
|
{formatText(profile.description)}
|
||||||
dangerouslySetInnerHTML={{ __html: formatText(profile.description) }}
|
</Markdown>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import markdown from '~/styles/common/markdown.module.scss';
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
|
|
||||||
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
||||||
|
import { ProfileToggles } from '../ProfileToggles';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
@ -40,9 +41,19 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
<Filler className={classNames(markdown.wrapper, styles.text)}>
|
<Filler className={classNames(markdown.wrapper, styles.text)}>
|
||||||
<Group>
|
<Group>
|
||||||
<VerticalMenu className={styles.menu}>
|
<VerticalMenu className={styles.menu}>
|
||||||
<VerticalMenu.Item onClick={() => setActiveTab(0)}>Настройки</VerticalMenu.Item>
|
<VerticalMenu.Item onClick={() => setActiveTab(0)}>
|
||||||
|
Настройки
|
||||||
|
</VerticalMenu.Item>
|
||||||
|
|
||||||
|
<VerticalMenu.Item onClick={() => setActiveTab(1)}>
|
||||||
|
Заметки
|
||||||
|
</VerticalMenu.Item>
|
||||||
</VerticalMenu>
|
</VerticalMenu>
|
||||||
|
|
||||||
|
<div className={styles.toggles}>
|
||||||
|
<ProfileToggles />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.stats}>
|
<div className={styles.stats}>
|
||||||
<ProfileStats />
|
<ProfileStats />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,3 +19,7 @@
|
||||||
.stats {
|
.stats {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggles {
|
||||||
|
padding-top: $gap * 2;
|
||||||
|
}
|
||||||
|
|
17
src/containers/profile/ProfileToggles/index.tsx
Normal file
17
src/containers/profile/ProfileToggles/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Zone } from '~/components/containers/Zone';
|
||||||
|
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||||
|
|
||||||
|
interface ProfileTogglesProps {}
|
||||||
|
|
||||||
|
const ProfileToggles: FC<ProfileTogglesProps> = () => (
|
||||||
|
<Zone>
|
||||||
|
<Group>
|
||||||
|
<SuperPowersToggle />
|
||||||
|
</Group>
|
||||||
|
</Zone>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ProfileToggles };
|
|
@ -1,56 +1,80 @@
|
||||||
import React, { useState, VFC } from 'react';
|
import { FC, useCallback, useState, VFC } from 'react';
|
||||||
|
|
||||||
import { Card } from '~/components/containers/Card';
|
|
||||||
import { Columns } from '~/components/containers/Columns';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Padder } from '~/components/containers/Padder';
|
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import { InputText } from '~/components/input/InputText';
|
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
|
||||||
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
|
|
||||||
import { NoteCard } from '~/components/notes/NoteCard';
|
import { NoteCard } from '~/components/notes/NoteCard';
|
||||||
import { useGetNotes } from '~/hooks/notes/useGetNotes';
|
import { NoteCreationForm } from '~/components/notes/NoteCreationForm';
|
||||||
|
import { useConfirmation } from '~/hooks/dom/useConfirmation';
|
||||||
|
import { NoteProvider, useNotesContext } from '~/utils/providers/NoteProvider';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface SettingsNotesProps {}
|
interface SettingsNotesProps {}
|
||||||
|
|
||||||
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
const List = () => {
|
||||||
const [text, setText] = useState('');
|
const { notes, remove, update } = useNotesContext();
|
||||||
const { notes } = useGetNotes('');
|
const confirm = useConfirmation();
|
||||||
|
|
||||||
|
const onRemove = useCallback(
|
||||||
|
async (id: number) => {
|
||||||
|
confirm('Удалить? Это удалит заметку навсегда', () => remove(id));
|
||||||
|
},
|
||||||
|
[remove],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Padder>
|
|
||||||
<Group horizontal>
|
|
||||||
<HorizontalMenu>
|
|
||||||
<HorizontalMenu.Item active>Новые</HorizontalMenu.Item>
|
|
||||||
<HorizontalMenu.Item>Старые</HorizontalMenu.Item>
|
|
||||||
</HorizontalMenu>
|
|
||||||
|
|
||||||
<Filler />
|
|
||||||
|
|
||||||
<InputText suffix={<Icon icon="search" size={24} />} />
|
|
||||||
</Group>
|
|
||||||
</Padder>
|
|
||||||
|
|
||||||
<Columns>
|
|
||||||
<Card>
|
|
||||||
<Group>
|
|
||||||
<Textarea handler={setText} value={text} />
|
|
||||||
|
|
||||||
<Group horizontal>
|
|
||||||
<Filler />
|
|
||||||
<Button size="mini">Добавить</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{notes.map(note => (
|
{notes.map(note => (
|
||||||
<NoteCard key={note.id} content={note.content} createdAt={note.created_at} />
|
<NoteCard
|
||||||
|
remove={() => onRemove(note.id)}
|
||||||
|
update={(text, callback) => update(note.id, text, callback)}
|
||||||
|
key={note.id}
|
||||||
|
content={note.content}
|
||||||
|
createdAt={note.created_at}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Columns>
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form: FC<{ onCancel: () => void }> = ({ onCancel }) => {
|
||||||
|
const { create: submit } = useNotesContext();
|
||||||
|
return <NoteCreationForm onSubmit={submit} onCancel={onCancel} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
||||||
|
const [formIsShown, setFormIsShown] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NoteProvider>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<div className={styles.head}>
|
||||||
|
{formIsShown ? (
|
||||||
|
<Form onCancel={() => setFormIsShown(false)} />
|
||||||
|
) : (
|
||||||
|
<Group className={styles.showForm} horizontal>
|
||||||
|
<Filler />
|
||||||
|
<Button
|
||||||
|
onClick={() => setFormIsShown(true)}
|
||||||
|
size="mini"
|
||||||
|
iconRight="plus"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.list}>
|
||||||
|
<Group>
|
||||||
|
<Group>
|
||||||
|
<List />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NoteProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 4;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
@include row_shadow;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
@include row_shadow;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Superpower } from "~/components/boris/Superpower";
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
import { Filler } from "~/components/containers/Filler";
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from "~/components/containers/Group";
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Zone } from "~/components/containers/Zone";
|
import { Zone } from '~/components/containers/Zone';
|
||||||
import { InputText } from "~/components/input/InputText";
|
import { InputText } from '~/components/input/InputText';
|
||||||
import { Textarea } from "~/components/input/Textarea";
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import { ERROR_LITERAL } from "~/constants/errors";
|
import { ERROR_LITERAL } from '~/constants/errors';
|
||||||
import { ProfileAccounts } from "~/containers/profile/ProfileAccounts";
|
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
|
||||||
import { useSettings } from "~/utils/providers/SettingsProvider";
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
import { has } from "~/utils/ramda";
|
import { useSettings } from '~/utils/providers/SettingsProvider';
|
||||||
|
import { has } from '~/utils/ramda';
|
||||||
|
|
||||||
import styles from "./styles.module.scss";
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface UserSettingsViewProps {}
|
interface UserSettingsViewProps {}
|
||||||
|
|
||||||
|
@ -20,10 +21,11 @@ const getError = (error?: string) =>
|
||||||
|
|
||||||
const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
||||||
const { values, handleChange, errors } = useSettings();
|
const { values, handleChange, errors } = useSettings();
|
||||||
|
const { isPhone } = useWindowSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
<Group horizontal className={styles.base_info}>
|
<Group horizontal={!isPhone} className={styles.base_info}>
|
||||||
<Superpower>
|
<Superpower>
|
||||||
<Zone className={styles.avatar} title="Фото">
|
<Zone className={styles.avatar} title="Фото">
|
||||||
<small>
|
<small>
|
||||||
|
@ -33,18 +35,18 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
||||||
</Zone>
|
</Zone>
|
||||||
</Superpower>
|
</Superpower>
|
||||||
|
|
||||||
<Zone title="О себе">
|
<Zone title="О себе" className={styles.about}>
|
||||||
<Group>
|
<Group>
|
||||||
<InputText
|
<InputText
|
||||||
value={values.fullname}
|
value={values.fullname}
|
||||||
handler={handleChange("fullname")}
|
handler={handleChange('fullname')}
|
||||||
title="Полное имя"
|
title="Полное имя"
|
||||||
error={getError(errors.fullname)}
|
error={getError(errors.fullname)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
value={values.description}
|
value={values.description}
|
||||||
handler={handleChange("description")}
|
handler={handleChange('description')}
|
||||||
title="Описание"
|
title="Описание"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -75,21 +77,21 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
||||||
<Group>
|
<Group>
|
||||||
<InputText
|
<InputText
|
||||||
value={values.username}
|
value={values.username}
|
||||||
handler={handleChange("username")}
|
handler={handleChange('username')}
|
||||||
title="Логин"
|
title="Логин"
|
||||||
error={getError(errors.username)}
|
error={getError(errors.username)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={values.email}
|
value={values.email}
|
||||||
handler={handleChange("email")}
|
handler={handleChange('email')}
|
||||||
title="E-mail"
|
title="E-mail"
|
||||||
error={getError(errors.email)}
|
error={getError(errors.email)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={values.newPassword}
|
value={values.newPassword}
|
||||||
handler={handleChange("newPassword")}
|
handler={handleChange('newPassword')}
|
||||||
title="Новый пароль"
|
title="Новый пароль"
|
||||||
type="password"
|
type="password"
|
||||||
error={getError(errors.newPassword)}
|
error={getError(errors.newPassword)}
|
||||||
|
@ -97,7 +99,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={values.password}
|
value={values.password}
|
||||||
handler={handleChange("password")}
|
handler={handleChange('password')}
|
||||||
title="Старый пароль"
|
title="Старый пароль"
|
||||||
type="password"
|
type="password"
|
||||||
error={getError(errors.password)}
|
error={getError(errors.password)}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
$pad_danger: mix($red, $content_bg, 70%);
|
$pad_danger: mix($red, $content_bg, 70%);
|
||||||
$pad_usual: mix(white, $content_bg, 10%);
|
$pad_usual: mix(white, $content_bg, 10%);
|
||||||
|
|
||||||
|
.about {
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
@ -21,5 +25,5 @@ div.base_info.base_info {
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
flex: 0 0 150px;
|
flex: 1 0 90px;
|
||||||
}
|
}
|
|
@ -1,22 +1,72 @@
|
||||||
import { VFC } from 'react';
|
import React, { useCallback, useEffect, useMemo, VFC } from 'react';
|
||||||
|
|
||||||
|
import { isNil } from 'ramda';
|
||||||
|
|
||||||
|
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
|
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
|
||||||
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
|
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
|
||||||
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
||||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||||
|
import { SidebarName } from '~/constants/sidebar';
|
||||||
import { ProfileSidebarMenu } from '~/containers/profile/ProfileSidebarMenu';
|
import { ProfileSidebarMenu } from '~/containers/profile/ProfileSidebarMenu';
|
||||||
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
|
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
|
||||||
import { DialogComponentProps } from '~/types/modal';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
|
import { useUser } from '~/hooks/auth/useUser';
|
||||||
|
import type { SidebarComponentProps } from '~/types/sidebar';
|
||||||
|
|
||||||
interface ProfileSidebarProps extends DialogComponentProps {
|
const tabs = ['profile', 'bookmarks'] as const;
|
||||||
page: string;
|
type TabName = typeof tabs[number];
|
||||||
|
|
||||||
|
interface ProfileSidebarProps
|
||||||
|
extends SidebarComponentProps<SidebarName.Settings> {
|
||||||
|
page?: TabName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileSidebar: VFC<ProfileSidebarProps> = ({
|
||||||
|
onRequestClose,
|
||||||
|
page,
|
||||||
|
openSidebar,
|
||||||
|
}) => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
const {
|
||||||
|
user: { cover },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
|
const tab = useMemo(
|
||||||
|
() => (page ? Math.max(tabs.indexOf(page), 0) : undefined),
|
||||||
|
[page],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTabChange = useCallback(
|
||||||
|
(val: number | undefined) => {
|
||||||
|
openSidebar(SidebarName.Settings, {
|
||||||
|
page: !isNil(val) ? tabs[val] : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[open, onRequestClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUser) {
|
||||||
|
onRequestClose();
|
||||||
|
}
|
||||||
|
}, [isUser]);
|
||||||
|
|
||||||
|
if (!isUser) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileSidebar: VFC<ProfileSidebarProps> = ({ onRequestClose }) => {
|
|
||||||
return (
|
return (
|
||||||
<SidebarWrapper onClose={onRequestClose}>
|
<SidebarWrapper
|
||||||
<SidebarStack>
|
onClose={onRequestClose}
|
||||||
<SidebarStackCard headerFeature="close" title="Профиль" onBackPress={onRequestClose}>
|
backdrop={cover && <CoverBackdrop cover={cover} />}
|
||||||
|
>
|
||||||
|
<SidebarStack tab={tab} onTabChange={onTabChange}>
|
||||||
|
<SidebarStackCard
|
||||||
|
headerFeature="close"
|
||||||
|
title="Профиль"
|
||||||
|
onBackPress={onRequestClose}
|
||||||
|
>
|
||||||
<ProfileSidebarMenu onClose={onRequestClose} />
|
<ProfileSidebarMenu onClose={onRequestClose} />
|
||||||
</SidebarStackCard>
|
</SidebarStackCard>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
import React, { FC, ReactNode, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
|
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
|
||||||
|
|
||||||
|
@ -9,9 +9,15 @@ import styles from './styles.module.scss';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
closeOnBackdropClick?: boolean;
|
closeOnBackdropClick?: boolean;
|
||||||
|
backdrop?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarWrapper: FC<IProps> = ({ children, onClose, closeOnBackdropClick = true }) => {
|
const SidebarWrapper: FC<IProps> = ({
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
closeOnBackdropClick = true,
|
||||||
|
backdrop,
|
||||||
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useCloseOnEscape(onClose);
|
useCloseOnEscape(onClose);
|
||||||
|
@ -25,7 +31,12 @@ const SidebarWrapper: FC<IProps> = ({ children, onClose, closeOnBackdropClick =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} ref={ref}>
|
<div className={styles.wrapper} ref={ref}>
|
||||||
{closeOnBackdropClick && <div className={styles.backdrop} onClick={onClose} />}
|
{(closeOnBackdropClick || backdrop) && (
|
||||||
|
<div className={styles.backdrop} onClick={onClose}>
|
||||||
|
{backdrop}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import React, { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, {
|
||||||
|
ChangeEvent,
|
||||||
|
FC,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { TagAutocomplete } from '~/components/tags/TagAutocomplete';
|
import { TagAutocomplete } from '~/components/tags/TagAutocomplete';
|
||||||
import { TagWrapper } from '~/components/tags/TagWrapper';
|
import { TagWrapper } from '~/components/tags/TagWrapper';
|
||||||
|
@ -15,7 +23,7 @@ const prepareInput = (input: string): string[] => {
|
||||||
title
|
title
|
||||||
.trim()
|
.trim()
|
||||||
.substring(0, 64)
|
.substring(0, 64)
|
||||||
.toLowerCase()
|
.toLowerCase(),
|
||||||
)
|
)
|
||||||
.filter(el => el.length > 0);
|
.filter(el => el.length > 0);
|
||||||
};
|
};
|
||||||
|
@ -49,7 +57,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
|
|
||||||
setInput(items[items.length - 1] || '');
|
setInput(items[items.length - 1] || '');
|
||||||
},
|
},
|
||||||
[onAppend]
|
[onAppend],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
|
@ -69,14 +77,13 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
const created = prepareInput(input);
|
const created = prepareInput(input);
|
||||||
|
|
||||||
if (created.length) {
|
if (created.length) {
|
||||||
console.log('appending?!!')
|
|
||||||
onAppend(created);
|
onAppend(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInput('');
|
setInput('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[input, setInput, onClearTag, onAppend]
|
[input, setInput, onClearTag, onAppend],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFocus = useCallback(() => setFocused(true), []);
|
const onFocus = useCallback(() => setFocused(true), []);
|
||||||
|
@ -99,7 +106,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
|
|
||||||
onSubmit([]);
|
onSubmit([]);
|
||||||
},
|
},
|
||||||
[input, setInput, onSubmit]
|
[input, setInput, onSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAutocompleteSelect = useCallback(
|
const onAutocompleteSelect = useCallback(
|
||||||
|
@ -112,10 +119,12 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
|
|
||||||
onAppend([val]);
|
onAppend([val]);
|
||||||
},
|
},
|
||||||
[onAppend, setInput]
|
[onAppend, setInput],
|
||||||
);
|
);
|
||||||
|
|
||||||
const feature = useMemo(() => (input?.substr(0, 1) === '/' ? 'green' : ''), [input]);
|
const feature = useMemo(() => (input?.substr(0, 1) === '/' ? 'green' : ''), [
|
||||||
|
input,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!focused) return;
|
if (!focused) return;
|
||||||
|
@ -126,7 +135,11 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap} ref={wrapper}>
|
<div className={styles.wrap} ref={wrapper}>
|
||||||
<TagWrapper title={input || placeholder} hasInput={true} feature={feature}>
|
<TagWrapper
|
||||||
|
title={input || placeholder}
|
||||||
|
hasInput={true}
|
||||||
|
feature={feature}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
|
|
|
@ -35,14 +35,6 @@ export const useMessageEventReactions = () => {
|
||||||
void createSocialAccount(path(['data', 'payload', 'token'], event));
|
void createSocialAccount(path(['data', 'payload', 'token'], event));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EventMessageType.OpenProfile:
|
|
||||||
const username: string | undefined = path(['data', 'username'], event);
|
|
||||||
if (!username) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showModal(Dialog.Profile, { username });
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.log('unknown message', event.data);
|
console.log('unknown message', event.data);
|
||||||
}
|
}
|
||||||
|
|
9
src/hooks/auth/useSuperPowers.ts
Normal file
9
src/hooks/auth/useSuperPowers.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useAuth } from "~/hooks/auth/useAuth";
|
||||||
|
|
||||||
|
export const useSuperPowers = () => {
|
||||||
|
const { isTester, setIsTester } = useAuth();
|
||||||
|
|
||||||
|
return useMemo(() => ({ isTester, setIsTester }), [isTester, setIsTester]);
|
||||||
|
};
|
|
@ -1,18 +1,16 @@
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from "date-fns/isBefore";
|
||||||
|
|
||||||
import { useRandomPhrase } from '~/constants/phrases';
|
import { useRandomPhrase } from "~/constants/phrases";
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useLastSeenBoris } from "~/hooks/auth/useLastSeenBoris";
|
||||||
import { useLastSeenBoris } from '~/hooks/auth/useLastSeenBoris';
|
import { useBorisStats } from "~/hooks/boris/useBorisStats";
|
||||||
import { useBorisStats } from '~/hooks/boris/useBorisStats';
|
import { IComment } from "~/types";
|
||||||
import { IComment } from '~/types';
|
|
||||||
|
|
||||||
export const useBoris = (comments: IComment[]) => {
|
export const useBoris = (comments: IComment[]) => {
|
||||||
const title = useRandomPhrase('BORIS_TITLE');
|
const title = useRandomPhrase("BORIS_TITLE");
|
||||||
|
|
||||||
const { lastSeen, setLastSeen } = useLastSeenBoris();
|
const { lastSeen, setLastSeen } = useLastSeenBoris();
|
||||||
const { isTester, setIsTester } = useAuth();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const last_comment = comments[0];
|
const last_comment = comments[0];
|
||||||
|
@ -32,12 +30,5 @@ export const useBoris = (comments: IComment[]) => {
|
||||||
|
|
||||||
const { stats, isLoading: isLoadingStats } = useBorisStats();
|
const { stats, isLoading: isLoadingStats } = useBorisStats();
|
||||||
|
|
||||||
const setIsBetaTester = useCallback(
|
return { stats, title, isLoadingStats };
|
||||||
(isTester: boolean) => {
|
|
||||||
setIsTester(isTester);
|
|
||||||
},
|
|
||||||
[setIsTester]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { setIsBetaTester, isTester, stats, title, isLoadingStats };
|
|
||||||
};
|
};
|
||||||
|
|
11
src/hooks/dom/useConfirmation.ts
Normal file
11
src/hooks/dom/useConfirmation.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
export const useConfirmation = () =>
|
||||||
|
useCallback((prompt = "", onApprove: () => {}, onReject?: () => {}) => {
|
||||||
|
if (!window.confirm(prompt || "Уверен?")) {
|
||||||
|
onReject?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onApprove();
|
||||||
|
}, []);
|
|
@ -1,24 +1,30 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
export const useWindowSize = () => {
|
export const useWindowSize = () => {
|
||||||
const [size, setSize] = useState({ innerWidth: 0, innerHeight: 0, isMobile: false });
|
const [size, setSize] = useState({
|
||||||
|
innerWidth: 0,
|
||||||
|
innerHeight: 0,
|
||||||
|
isTablet: false,
|
||||||
|
isPhone: false,
|
||||||
|
});
|
||||||
|
|
||||||
const onResize = useCallback(
|
const onResize = useCallback(
|
||||||
() =>
|
() =>
|
||||||
setSize({
|
setSize({
|
||||||
innerWidth: window.innerWidth,
|
innerWidth: window.innerWidth,
|
||||||
innerHeight: window.innerHeight,
|
innerHeight: window.innerHeight,
|
||||||
isMobile: window.innerWidth < 768,
|
isTablet: window.innerWidth < 768,
|
||||||
|
isPhone: window.innerWidth < 500,
|
||||||
}),
|
}),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onResize();
|
onResize();
|
||||||
|
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
return () => window.removeEventListener('resize', onResize);
|
return () => window.removeEventListener("resize", onResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return size;
|
return size;
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
|
|
||||||
import { apiGetNotes } from '~/api/notes';
|
|
||||||
import { ApiGetNotesRequest } from '~/api/notes/types';
|
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
|
||||||
import { GetLabNodesRequest, ILabNode } from '~/types/lab';
|
|
||||||
import { flatten, uniqBy } from '~/utils/ramda';
|
|
||||||
|
|
||||||
const DEFAULT_COUNT = 20;
|
|
||||||
|
|
||||||
const getKey: (isUser: boolean, search: string) => SWRInfiniteKeyLoader = (isUser, search) => (
|
|
||||||
index,
|
|
||||||
prev: ILabNode[]
|
|
||||||
) => {
|
|
||||||
if (!isUser) return null;
|
|
||||||
if (index > 0 && (!prev?.length || prev.length < 20)) return null;
|
|
||||||
|
|
||||||
const props: GetLabNodesRequest = {
|
|
||||||
limit: DEFAULT_COUNT,
|
|
||||||
offset: index * DEFAULT_COUNT,
|
|
||||||
search: search || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify(props);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseKey = (key: string): ApiGetNotesRequest => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(key);
|
|
||||||
} catch (error) {
|
|
||||||
return { limit: DEFAULT_COUNT, offset: 0, search: '' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetNotes = (search: string) => {
|
|
||||||
const { isUser } = useAuth();
|
|
||||||
|
|
||||||
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
|
|
||||||
getKey(isUser, search),
|
|
||||||
async (key: string) => {
|
|
||||||
const result = await apiGetNotes(parseKey(key));
|
|
||||||
return result.list;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dedupingInterval: 300,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const notes = useMemo(() => uniqBy(n => n.id, flatten(data || [])), [data]);
|
|
||||||
const hasMore = (data?.[size - 1]?.length || 0) >= 1;
|
|
||||||
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
|
||||||
|
|
||||||
return { notes, hasMore, loadMore, isLoading: !data && isValidating };
|
|
||||||
};
|
|
113
src/hooks/notes/useNotes.ts
Normal file
113
src/hooks/notes/useNotes.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
import useSWRInfinite, { SWRInfiniteKeyLoader } from "swr/infinite";
|
||||||
|
|
||||||
|
import {
|
||||||
|
apiCreateNote,
|
||||||
|
apiDeleteNote,
|
||||||
|
apiListNotes,
|
||||||
|
apiUpdateNote,
|
||||||
|
} from "~/api/notes";
|
||||||
|
import { ApiGetNotesRequest } from "~/api/notes/types";
|
||||||
|
import { useAuth } from "~/hooks/auth/useAuth";
|
||||||
|
import { GetLabNodesRequest, ILabNode } from "~/types/lab";
|
||||||
|
import { Note } from "~/types/notes";
|
||||||
|
import { flatten, uniqBy } from "~/utils/ramda";
|
||||||
|
|
||||||
|
const DEFAULT_COUNT = 20;
|
||||||
|
|
||||||
|
const getKey: (isUser: boolean, search: string) => SWRInfiniteKeyLoader = (
|
||||||
|
isUser,
|
||||||
|
search,
|
||||||
|
) => (index, prev: ILabNode[]) => {
|
||||||
|
if (!isUser) return null;
|
||||||
|
if (index > 0 && (!prev?.length || prev.length < 20)) return null;
|
||||||
|
|
||||||
|
const props: GetLabNodesRequest = {
|
||||||
|
limit: DEFAULT_COUNT,
|
||||||
|
offset: index * DEFAULT_COUNT,
|
||||||
|
search: search || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseKey = (key: string): ApiGetNotesRequest => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(key);
|
||||||
|
} catch (error) {
|
||||||
|
return { limit: DEFAULT_COUNT, offset: 0, search: "" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNotes = (search: string) => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
|
||||||
|
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
|
||||||
|
getKey(isUser, search),
|
||||||
|
async (key: string) => {
|
||||||
|
const result = await apiListNotes(parseKey(key));
|
||||||
|
return result.list;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dedupingInterval: 300,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const create = useCallback(
|
||||||
|
async (text: string, onSuccess?: (note: Note) => void) => {
|
||||||
|
const result = await apiCreateNote({ text });
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
await mutate(
|
||||||
|
data?.map((it, index) => (index === 0 ? [result, ...it] : it)),
|
||||||
|
{ revalidate: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess?.(result);
|
||||||
|
},
|
||||||
|
[mutate, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const remove = useCallback(
|
||||||
|
async (id: number, onSuccess?: () => void) => {
|
||||||
|
await apiDeleteNote(id);
|
||||||
|
await mutate(
|
||||||
|
data?.map(page => page.filter(it => it.id !== id)),
|
||||||
|
{ revalidate: false },
|
||||||
|
);
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
[mutate, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
async (id: number, text: string, onSuccess?: () => void) => {
|
||||||
|
const result = await apiUpdateNote({ id, text });
|
||||||
|
await mutate(
|
||||||
|
data?.map(page => page.map(it => (it.id === id ? result : it))),
|
||||||
|
{ revalidate: false },
|
||||||
|
);
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
[mutate, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const notes = useMemo(() => uniqBy(n => n.id, flatten(data || [])), [data]);
|
||||||
|
const hasMore = (data?.[size - 1]?.length || 0) >= 1;
|
||||||
|
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
notes,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
isLoading: !data && isValidating,
|
||||||
|
create,
|
||||||
|
remove,
|
||||||
|
update,
|
||||||
|
}),
|
||||||
|
[notes, hasMore, loadMore, data, isValidating, create, remove],
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,12 +3,12 @@ import { FC, useMemo } from 'react';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
import { BorisGraphicStats } from '~/components/boris/BorisGraphicStats';
|
import { BorisGraphicStats } from '~/components/boris/BorisGraphicStats';
|
||||||
import { BorisSidebar } from '~/components/boris/BorisSidebar';
|
|
||||||
import { Superpower } from '~/components/boris/Superpower';
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from '~/components/containers/Card';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Sticky } from '~/components/containers/Sticky';
|
import { Sticky } from '~/components/containers/Sticky';
|
||||||
import { BorisComments } from '~/containers/boris/BorisComments';
|
import { BorisComments } from '~/containers/boris/BorisComments';
|
||||||
|
import { BorisSidebar } from '~/containers/boris/BorisSidebar';
|
||||||
import { BorisSuperPowersSSR } from '~/containers/boris/BorisSuperpowers/ssr';
|
import { BorisSuperPowersSSR } from '~/containers/boris/BorisSuperpowers/ssr';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
|
@ -19,21 +19,20 @@ import styles from './styles.module.scss';
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
title: string;
|
title: string;
|
||||||
setIsBetaTester: (val: boolean) => void;
|
|
||||||
isTester: boolean;
|
|
||||||
stats: BorisUsageStats;
|
stats: BorisUsageStats;
|
||||||
isLoadingStats: boolean;
|
isLoadingStats: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BorisLayout: FC<IProps> = observer(
|
const BorisLayout: FC<IProps> = observer(({ title, stats, isLoadingStats }) => {
|
||||||
({ title, setIsBetaTester, isTester, stats, isLoadingStats }) => {
|
|
||||||
const { isUser } = useAuthProvider();
|
const { isUser } = useAuthProvider();
|
||||||
const commentsByMonth = useMemo(() => stats.backend.comments.by_month?.slice(0, -1), [
|
const commentsByMonth = useMemo(
|
||||||
stats.backend.comments.by_month,
|
() => stats.backend.comments.by_month?.slice(0, -1),
|
||||||
]);
|
[stats.backend.comments.by_month],
|
||||||
const nodesByMonth = useMemo(() => stats.backend.nodes.by_month?.slice(0, -1), [
|
);
|
||||||
stats.backend.comments.by_month,
|
const nodesByMonth = useMemo(
|
||||||
]);
|
() => stats.backend.nodes.by_month?.slice(0, -1),
|
||||||
|
[stats.backend.comments.by_month],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -51,9 +50,11 @@ const BorisLayout: FC<IProps> = observer(
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Card className={styles.content}>
|
<Card className={styles.content}>
|
||||||
<Group>
|
<Group>
|
||||||
|
<div>
|
||||||
<Superpower>
|
<Superpower>
|
||||||
<BorisSuperPowersSSR />
|
<BorisSuperPowersSSR />
|
||||||
</Superpower>
|
</Superpower>
|
||||||
|
</div>
|
||||||
|
|
||||||
<BorisGraphicStats
|
<BorisGraphicStats
|
||||||
totalComments={stats.backend.comments.total}
|
totalComments={stats.backend.comments.total}
|
||||||
|
@ -69,9 +70,7 @@ const BorisLayout: FC<IProps> = observer(
|
||||||
<Group className={styles.stats}>
|
<Group className={styles.stats}>
|
||||||
<Sticky>
|
<Sticky>
|
||||||
<BorisSidebar
|
<BorisSidebar
|
||||||
isTester={isTester}
|
|
||||||
stats={stats}
|
stats={stats}
|
||||||
setBetaTester={setIsBetaTester}
|
|
||||||
isUser={isUser}
|
isUser={isUser}
|
||||||
isLoading={isLoadingStats}
|
isLoading={isLoadingStats}
|
||||||
/>
|
/>
|
||||||
|
@ -83,7 +82,6 @@ const BorisLayout: FC<IProps> = observer(
|
||||||
<SidebarRouter prefix="/" />
|
<SidebarRouter prefix="/" />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export { BorisLayout };
|
export { BorisLayout };
|
||||||
|
|
|
@ -25,7 +25,7 @@ const BorisPage: VFC = observer(() => {
|
||||||
isLoading: isLoadingComments,
|
isLoading: isLoadingComments,
|
||||||
isLoadingMore,
|
isLoadingMore,
|
||||||
} = useNodeComments(696);
|
} = useNodeComments(696);
|
||||||
const { title, setIsBetaTester, isTester, stats, isLoadingStats } = useBoris(comments);
|
const { title, stats, isLoadingStats } = useBoris(comments);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
||||||
|
@ -43,8 +43,6 @@ const BorisPage: VFC = observer(() => {
|
||||||
|
|
||||||
<BorisLayout
|
<BorisLayout
|
||||||
title={title}
|
title={title}
|
||||||
setIsBetaTester={setIsBetaTester}
|
|
||||||
isTester={isTester}
|
|
||||||
stats={stats}
|
stats={stats}
|
||||||
isLoadingStats={isLoadingStats}
|
isLoadingStats={isLoadingStats}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import "src/styles/variables";
|
@import 'src/styles/variables';
|
||||||
@import 'photoswipe/dist/photoswipe';
|
@import 'photoswipe/dist/photoswipe';
|
||||||
@import 'photoswipe/dist/default-skin/default-skin';
|
@import 'photoswipe/dist/default-skin/default-skin';
|
||||||
|
|
||||||
|
@ -55,7 +55,8 @@ body {
|
||||||
color: #555555;
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo, .done {
|
.todo,
|
||||||
|
.done {
|
||||||
color: #333333;
|
color: #333333;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
|
@ -76,12 +77,8 @@ h2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
background: transparentize($color: #000000, $amount: 0.8);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $wisegreen;
|
color: $wisegreen;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -129,12 +126,16 @@ button {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3, h2, h1 {
|
h3,
|
||||||
|
h2,
|
||||||
|
h1 {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6, h5, h4 {
|
h6,
|
||||||
|
h5,
|
||||||
|
h4 {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,87 @@
|
||||||
License: none (public domain)
|
License: none (public domain)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
html, body, div, span, applet, object, iframe,
|
html,
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
body,
|
||||||
a, abbr, acronym, address, big, cite, code,
|
div,
|
||||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
span,
|
||||||
small, strike, strong, sub, sup, tt, var,
|
applet,
|
||||||
b, u, i, center,
|
object,
|
||||||
dl, dt, dd, ol, ul, li,
|
iframe,
|
||||||
fieldset, form, label, legend,
|
h1,
|
||||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
h2,
|
||||||
article, aside, canvas, details, embed,
|
h3,
|
||||||
figure, figcaption, footer, header, hgroup,
|
h4,
|
||||||
menu, nav, output, ruby, section, summary,
|
h5,
|
||||||
time, mark, audio, video {
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -24,21 +92,34 @@ time, mark, audio, video {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
/* HTML5 display-role reset for older browsers */
|
/* HTML5 display-role reset for older browsers */
|
||||||
article, aside, details, figcaption, figure,
|
article,
|
||||||
footer, header, hgroup, menu, nav, section {
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
ol, ul {
|
ol,
|
||||||
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
blockquote, q {
|
blockquote,
|
||||||
|
q {
|
||||||
quotes: none;
|
quotes: none;
|
||||||
}
|
}
|
||||||
blockquote:before, blockquote:after,
|
blockquote:before,
|
||||||
q:before, q:after {
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
content: '';
|
content: '';
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
@ -48,4 +129,5 @@ table {
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { ERRORS } from '~/constants/errors';
|
import { Context } from "react";
|
||||||
import { IUser } from '~/types/auth';
|
|
||||||
|
import { ERRORS } from "~/constants/errors";
|
||||||
|
import { IUser } from "~/types/auth";
|
||||||
|
|
||||||
export interface ITag {
|
export interface ITag {
|
||||||
ID: number;
|
ID: number;
|
||||||
|
@ -16,10 +18,11 @@ export interface ITag {
|
||||||
export type IIcon = string;
|
export type IIcon = string;
|
||||||
|
|
||||||
export type ValueOf<T> = T[keyof T];
|
export type ValueOf<T> = T[keyof T];
|
||||||
|
export type ContextValue<T> = T extends Context<infer U> ? U : never;
|
||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
export type IUploadType = "image" | "text" | "audio" | "video" | "other";
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -52,17 +55,21 @@ export interface IFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockText {
|
export interface IBlockText {
|
||||||
type: 'text';
|
type: "text";
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockEmbed {
|
export interface IBlockEmbed {
|
||||||
type: 'video';
|
type: "video";
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IBlock = IBlockText | IBlockEmbed;
|
export type IBlock = IBlockText | IBlockEmbed;
|
||||||
export type FlowDisplayVariant = 'single' | 'vertical' | 'horizontal' | 'quadro';
|
export type FlowDisplayVariant =
|
||||||
|
| "single"
|
||||||
|
| "vertical"
|
||||||
|
| "horizontal"
|
||||||
|
| "quadro";
|
||||||
export interface FlowDisplay {
|
export interface FlowDisplay {
|
||||||
display: FlowDisplayVariant;
|
display: FlowDisplayVariant;
|
||||||
show_description: boolean;
|
show_description: boolean;
|
||||||
|
@ -102,7 +109,7 @@ export interface INode {
|
||||||
|
|
||||||
export type IFlowNode = Pick<
|
export type IFlowNode = Pick<
|
||||||
INode,
|
INode,
|
||||||
'id' | 'flow' | 'description' | 'title' | 'thumbnail' | 'created_at'
|
"id" | "flow" | "description" | "title" | "thumbnail" | "created_at"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface IComment {
|
export interface IComment {
|
||||||
|
@ -116,7 +123,7 @@ export interface IComment {
|
||||||
deleted_at?: string;
|
deleted_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IMessage = Omit<IComment, 'user' | 'node'> & {
|
export type IMessage = Omit<IComment, "user" | "node"> & {
|
||||||
from: IUser;
|
from: IUser;
|
||||||
to: IUser;
|
to: IUser;
|
||||||
};
|
};
|
||||||
|
@ -125,7 +132,7 @@ export interface ICommentGroup {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
distancesInDays: number[];
|
distancesInDays: number[];
|
||||||
ids: IComment['id'][];
|
ids: IComment["id"][];
|
||||||
hasNew: boolean;
|
hasNew: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,19 +140,19 @@ export type IUploadProgressHandler = (progress: ProgressEvent) => void;
|
||||||
export type IError = ValueOf<typeof ERRORS>;
|
export type IError = ValueOf<typeof ERRORS>;
|
||||||
|
|
||||||
export const NOTIFICATION_TYPES = {
|
export const NOTIFICATION_TYPES = {
|
||||||
message: 'message',
|
message: "message",
|
||||||
comment: 'comment',
|
comment: "comment",
|
||||||
node: 'node',
|
node: "node",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IMessageNotification = {
|
export type IMessageNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES['message'];
|
type: typeof NOTIFICATION_TYPES["message"];
|
||||||
content: Partial<IMessage>;
|
content: Partial<IMessage>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ICommentNotification = {
|
export type ICommentNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES['comment'];
|
type: typeof NOTIFICATION_TYPES["comment"];
|
||||||
content: Partial<IComment>;
|
content: Partial<IComment>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
20
src/types/sidebar/index.ts
Normal file
20
src/types/sidebar/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import type { SidebarComponents } from "~/constants/sidebar/components";
|
||||||
|
|
||||||
|
export type SidebarComponent = keyof SidebarComponents;
|
||||||
|
|
||||||
|
// TODO: use it to store props for sidebar
|
||||||
|
export type SidebarProps<
|
||||||
|
T extends SidebarComponent
|
||||||
|
> = SidebarComponents[T] extends FunctionComponent<infer U>
|
||||||
|
? U extends object
|
||||||
|
? U extends SidebarComponentProps<T>
|
||||||
|
? Omit<U, keyof SidebarComponentProps<T>>
|
||||||
|
: U
|
||||||
|
: U
|
||||||
|
: {};
|
||||||
|
export interface SidebarComponentProps<T extends SidebarComponent> {
|
||||||
|
onRequestClose: () => void;
|
||||||
|
openSidebar: (name: T, props: SidebarProps<T>) => void;
|
||||||
|
}
|
|
@ -9,22 +9,20 @@ import { stripHTMLTags } from '~/utils/stripHTMLTags';
|
||||||
export const formatTextSanitizeYoutube = (text: string): string =>
|
export const formatTextSanitizeYoutube = (text: string): string =>
|
||||||
text.replace(
|
text.replace(
|
||||||
/(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-&=]+)/gim,
|
/(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-&=]+)/gim,
|
||||||
'\n$1\n'
|
'\n$1\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes HTML tags
|
* Removes HTML tags
|
||||||
*/
|
*/
|
||||||
export const formatTextSanitizeTags = (text: string): string => stripHTMLTags(text);
|
export const formatTextSanitizeTags = (text: string): string =>
|
||||||
|
stripHTMLTags(text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns clickable usernames
|
* Returns clickable usernames
|
||||||
*/
|
*/
|
||||||
export const formatTextClickableUsernames = (text: string): string =>
|
export const formatTextClickableUsernames = (text: string): string =>
|
||||||
text.replace(
|
text.replace(/~([\wа-яА-Я-]+)/giu, `<span class="username">~$1</span>`);
|
||||||
/~([\wа-яА-Я-]+)/giu,
|
|
||||||
`<span class="username" onClick="window.postMessage({ type: '${EventMessageType.OpenProfile}', username: '$1'});">~$1</span>`
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes gray comments
|
* Makes gray comments
|
||||||
|
@ -41,10 +39,13 @@ export const formatTextComments = (text: string): string =>
|
||||||
*/
|
*/
|
||||||
export const formatTextTodos = (text: string): string =>
|
export const formatTextTodos = (text: string): string =>
|
||||||
text
|
text
|
||||||
.replace(/\/\/\s*(todo|туду):?\s*([^\n]+)/gim, '// <span class="todo">$1</span> $2')
|
.replace(
|
||||||
|
/\/\/\s*(todo|туду):?\s*([^\n]+)/gim,
|
||||||
|
'// <span class="todo">$1</span> $2',
|
||||||
|
)
|
||||||
.replace(
|
.replace(
|
||||||
/\/\/\s*(done|сделано|сделал|готово|fixed|пофикшено|фиксед):?\s*([^\n]+)/gim,
|
/\/\/\s*(done|сделано|сделал|готово|fixed|пофикшено|фиксед):?\s*([^\n]+)/gim,
|
||||||
'// <span class="done">$1</span> $2'
|
'// <span class="done">$1</span> $2',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +57,8 @@ export const formatExclamations = (text: string): string =>
|
||||||
/**
|
/**
|
||||||
* Replaces -- with dash
|
* Replaces -- with dash
|
||||||
*/
|
*/
|
||||||
export const formatTextDash = (text: string): string => text.replace(' -- ', ' — ');
|
export const formatTextDash = (text: string): string =>
|
||||||
|
text.replace(' -- ', ' — ');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats with markdown
|
* Formats with markdown
|
||||||
|
|
21
src/utils/providers/NoteProvider.tsx
Normal file
21
src/utils/providers/NoteProvider.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { createContext, FC, useContext } from 'react';
|
||||||
|
|
||||||
|
import { useNotes } from '~/hooks/notes/useNotes';
|
||||||
|
|
||||||
|
const NoteContext = createContext<ReturnType<typeof useNotes>>({
|
||||||
|
notes: [],
|
||||||
|
hasMore: false,
|
||||||
|
loadMore: async () => Promise.resolve(undefined),
|
||||||
|
isLoading: false,
|
||||||
|
create: () => Promise.resolve(),
|
||||||
|
remove: () => Promise.resolve(),
|
||||||
|
update: (id: number, text: string) => Promise.resolve(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoteProvider: FC = ({ children }) => {
|
||||||
|
const notes = useNotes('');
|
||||||
|
|
||||||
|
return <NoteContext.Provider value={notes}>{children}</NoteContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNotesContext = () => useContext(NoteContext);
|
|
@ -1,78 +1,98 @@
|
||||||
import { Context, createContext, createElement, FunctionComponent, PropsWithChildren, useCallback, useContext, useMemo } from 'react';
|
import {
|
||||||
|
Context,
|
||||||
|
createContext,
|
||||||
|
createElement,
|
||||||
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { has } from 'ramda';
|
import { has, omit } from 'ramda';
|
||||||
|
|
||||||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||||
import { sidebarComponents, SidebarName } from '~/constants/sidebar';
|
import { SidebarName } from '~/constants/sidebar';
|
||||||
import { DialogComponentProps } from '~/types/modal';
|
import { sidebarComponents } from '~/constants/sidebar/components';
|
||||||
|
import { SidebarComponent, SidebarProps } from '~/types/sidebar';
|
||||||
|
|
||||||
type ContextValue = typeof SidebarContext extends Context<infer U> ? U : never;
|
type ContextValue = typeof SidebarContext extends Context<infer U> ? U : never;
|
||||||
type Name = keyof typeof sidebarComponents;
|
|
||||||
|
|
||||||
// TODO: use it to store props for sidebar
|
|
||||||
type Props<T extends Name> = typeof sidebarComponents[T] extends FunctionComponent<infer U>
|
|
||||||
? U extends DialogComponentProps ? Omit<U, 'onRequestClose'> : U
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const SidebarContext = createContext({
|
const SidebarContext = createContext({
|
||||||
current: undefined as SidebarName | undefined,
|
current: undefined as SidebarName | undefined,
|
||||||
open: <T extends Name>(name: T) => {},
|
open: <T extends SidebarComponent>(name: T, props: SidebarProps<T>) => {},
|
||||||
close: () => {},
|
close: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SidebarProvider = <T extends Name>({ children }: PropsWithChildren<{}>) => {
|
export const SidebarProvider = <T extends SidebarComponent>({
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<{}>) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const current = useMemo(() => {
|
const current = useMemo(() => {
|
||||||
const val = router.query.sidebar as SidebarName | undefined
|
const val = router.query.sidebar as SidebarName | undefined;
|
||||||
|
|
||||||
return val && has(val, sidebarComponents) ? val : undefined;
|
return val && has(val, sidebarComponents) ? val : undefined;
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
<T extends Name>(name: T) => {
|
<T extends SidebarComponent>(name: T, props: SidebarProps<T>) => {
|
||||||
const [path] = router.asPath.split('?');
|
const [path] = router.asPath.split('?');
|
||||||
void router.push(path + '?sidebar=' + name, path + '?sidebar=' + name, {
|
const query = Object.entries(props as {})
|
||||||
|
.filter(([, val]) => val)
|
||||||
|
.map(([name, val]) => `${name}=${val}`)
|
||||||
|
.join('&');
|
||||||
|
const url = path + '?sidebar=' + name + (query && `&${query}`);
|
||||||
|
|
||||||
|
// don't store history inside the same sidebar
|
||||||
|
if (router.query?.sidebar === name) {
|
||||||
|
void router.replace(url, url, {
|
||||||
|
shallow: true,
|
||||||
|
scroll: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void router.push(url, url, {
|
||||||
shallow: true,
|
shallow: true,
|
||||||
scroll: false,
|
scroll: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[router]
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const close = useCallback(
|
const close = useCallback(() => {
|
||||||
() => {
|
|
||||||
const [path] = router.asPath.split('?');
|
const [path] = router.asPath.split('?');
|
||||||
|
|
||||||
console.log('trying to close');
|
|
||||||
|
|
||||||
void router.replace(path, path, {
|
void router.replace(path, path, {
|
||||||
shallow: true,
|
shallow: true,
|
||||||
scroll: false,
|
scroll: false,
|
||||||
});
|
});
|
||||||
},
|
}, [router]);
|
||||||
[router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = useMemo<ContextValue>(() => ({
|
const value = useMemo<ContextValue>(
|
||||||
|
() => ({
|
||||||
current,
|
current,
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
}), [current, open, close]);
|
}),
|
||||||
|
[current, open, close],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={value}>
|
<SidebarContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
{current &&
|
{current && (
|
||||||
<ModalWrapper onOverlayClick={close}>
|
<ModalWrapper onOverlayClick={close}>
|
||||||
{createElement(
|
{createElement(sidebarComponents[current], {
|
||||||
sidebarComponents[current],
|
onRequestClose: close,
|
||||||
{ onRequestClose: close } as any
|
openSidebar: open,
|
||||||
)}
|
...omit(['sidebar'], router.query),
|
||||||
|
} as any)}
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
}
|
)}
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useSidebar = () => useContext(SidebarContext);
|
export const useSidebar = () => useContext(SidebarContext);
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { EventMessageType } from '~/constants/events';
|
|
||||||
|
|
||||||
export const openUserProfile = (username?: string) => {
|
|
||||||
if (!username) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.postMessage({ type: EventMessageType.OpenProfile, username }, '*');
|
|
||||||
};
|
|
File diff suppressed because one or more lines are too long
|
@ -2260,10 +2260,10 @@ prelude-ls@^1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
prettier@^1.18.2:
|
prettier@^2.7.1:
|
||||||
version "1.19.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||||
|
|
||||||
pretty-format@^26.6.2:
|
pretty-format@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue