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

added superpowers toggle to sidebar

This commit is contained in:
Fedor Katurov 2022-08-05 21:23:18 +07:00
parent 32aaa1e8db
commit d652a76640
25 changed files with 400 additions and 283 deletions

View file

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

View file

@ -1,32 +0,0 @@
import React, { FC } from 'react';
import { BorisContacts } from '~/components/boris/BorisContacts';
import { BorisStats } from '~/components/boris/BorisStats';
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
import { Group } from '~/components/containers/Group';
import styles from '~/layouts/BorisLayout/styles.module.scss';
import { BorisUsageStats } from '~/types/boris';
interface Props {
isUser: boolean;
isTester: boolean;
stats: BorisUsageStats;
setBetaTester: (val: boolean) => void;
isLoading: boolean;
}
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading, isTester, setBetaTester }) => (
<Group className={styles.stats__container}>
<div className={styles.super_powers}>
{isUser && <BorisSuperpowers active={isTester} onChange={setBetaTester} />}
</div>
<BorisContacts />
<div className={styles.stats__wrap}>
<BorisStats stats={stats} isLoading={isLoading} />
</div>
</Group>
);
export { BorisSidebar };

View file

@ -1,15 +1,17 @@
import React, { FC } from 'react'; import React, { FC } from "react";
import { useAuth } from '~/hooks/auth/useAuth'; import { observer } from "mobx-react-lite";
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 };

View file

@ -16,7 +16,11 @@ const Zone: FC<ZoneProps> = ({
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>

View file

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

View file

@ -1,20 +1,20 @@
import React, { FC } from 'react'; import React, { 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 { EditorActionsPanel } from '~/components/editors/EditorActionsPanel'; import { EditorActionsPanel } from "~/components/editors/EditorActionsPanel";
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 { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik'; 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" }}>
<EditorActionsPanel /> <EditorActionsPanel />
<Group horizontal> <Group horizontal>
@ -22,17 +22,17 @@ const EditorButtons: FC = () => {
<InputText <InputText
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}
type="submit" type="submit"
/> />

View file

@ -1,9 +1,9 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from "react";
import { SortableImageGrid } from '~/components/sortable'; import { SortableImageGrid } from "~/components/sortable";
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { UploadStatus } from '~/store/uploader/UploaderStore'; import { UploadStatus } from "~/store/uploader/UploaderStore";
import { IFile } from '~/types'; import { IFile } from "~/types";
interface IProps { interface IProps {
files: IFile[]; files: IFile[];
@ -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}
/> />
); );
}; };

View file

@ -1,22 +1,22 @@
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from "react";
import classNames from 'classnames'; import classNames from "classnames";
import { Anchor } from '~/components/common/Anchor'; import { Anchor } from "~/components/common/Anchor";
import { MenuDots } from '~/components/common/MenuDots'; import { MenuDots } from "~/components/common/MenuDots";
import { CellShade } from '~/components/flow/CellShade'; import { CellShade } from "~/components/flow/CellShade";
import { FlowCellImage } from '~/components/flow/FlowCellImage'; import { FlowCellImage } from "~/components/flow/FlowCellImage";
import { FlowCellMenu } from '~/components/flow/FlowCellMenu'; import { FlowCellMenu } from "~/components/flow/FlowCellMenu";
import { FlowCellText } from '~/components/flow/FlowCellText'; import { FlowCellText } from "~/components/flow/FlowCellText";
import { useClickOutsideFocus } from '~/hooks/dom/useClickOutsideFocus'; import { useClickOutsideFocus } from "~/hooks/dom/useClickOutsideFocus";
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls'; import { useFlowCellControls } from "~/hooks/flow/useFlowCellControls";
import { FlowDisplay, INode } from '~/types'; import { FlowDisplay, INode } from "~/types";
import styles from './styles.module.scss'; import styles from "./styles.module.scss";
interface Props { interface Props {
id: INode['id']; id: INode["id"];
to: string; to: string;
title: string; title: string;
image?: string; image?: string;
@ -25,7 +25,7 @@ interface Props {
text?: string; text?: string;
flow: FlowDisplay; flow: FlowDisplay;
canEdit?: boolean; canEdit?: boolean;
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; onChangeCellView: (id: INode["id"], flow: FlowDisplay) => void;
} }
const FlowCell: FC<Props> = ({ const FlowCell: FC<Props> = ({
@ -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,21 +53,26 @@ 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") {
return 9; return 9;
} }
if (flow.display === 'horizontal') { if (flow.display === "horizontal") {
return 15; return 15;
} }
@ -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 && (

View file

@ -1,19 +1,19 @@
import React, { FC, useCallback, useMemo, useState } from 'react'; import React, { FC, useCallback, useMemo, useState } from "react";
import classNames from 'classnames'; import classNames from "classnames";
import SwiperCore, { Autoplay, EffectFade, Lazy, Navigation } from 'swiper'; import SwiperCore, { Autoplay, EffectFade, Lazy, Navigation } from "swiper";
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from "swiper/react";
import SwiperClass from 'swiper/types/swiper-class'; import SwiperClass from "swiper/types/swiper-class";
import { Icon } from '~/components/input/Icon'; import { Icon } from "~/components/input/Icon";
import { LoaderCircle } from '~/components/input/LoaderCircle'; import { LoaderCircle } from "~/components/input/LoaderCircle";
import { ImagePresets, URLS } from '~/constants/urls'; import { ImagePresets, URLS } from "~/constants/urls";
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useNavigation } from '~/hooks/navigation/useNavigation'; import { useNavigation } from "~/hooks/navigation/useNavigation";
import { IFlowNode } from '~/types'; import { IFlowNode } from "~/types";
import { getURLFromString } from '~/utils/dom'; import { getURLFromString } from "~/utils/dom";
import styles from './styles.module.scss'; import styles from "./styles.module.scss";
SwiperCore.use([EffectFade, Lazy, Autoplay, Navigation]); SwiperCore.use([EffectFade, Lazy, Autoplay, Navigation]);
@ -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) {
@ -135,7 +138,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
<img <img
src={getURLFromString(node.thumbnail!, preset)} src={getURLFromString(node.thumbnail!, preset)}
alt="" alt=""
className={classNames(styles.preview, 'swiper-lazy')} className={classNames(styles.preview, "swiper-lazy")}
/> />
</SwiperSlide> </SwiperSlide>
))} ))}

View file

@ -1,12 +1,12 @@
import React, { VFC } from 'react'; import React, { VFC } from "react";
import Tippy from '@tippyjs/react'; import Tippy from "@tippyjs/react";
import { Icon } from '~/components/input/Icon'; import { Icon } from "~/components/input/Icon";
import { MenuButton, MenuItemWithIcon, SeparatedMenu } from '~/components/menu'; import { MenuButton, MenuItemWithIcon, SeparatedMenu } from "~/components/menu";
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 NodeEditMenuProps { interface NodeEditMenuProps {
className?: string; className?: string;
@ -30,17 +30,20 @@ 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
{isHeroic ? 'Убрать с главной' : 'На главную'} icon={isHeroic ? "star_full" : "star"}
onClick={onStar}
>
{isHeroic ? "Убрать с главной" : "На главную"}
</MenuItemWithIcon> </MenuItemWithIcon>
)} )}
@ -48,8 +51,11 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
Редактировать Редактировать
</MenuItemWithIcon> </MenuItemWithIcon>
<MenuItemWithIcon icon={isLocked ? 'locked' : 'unlocked'} onClick={onLock}> <MenuItemWithIcon
{isLocked ? 'Восстановить' : 'Удалить'} icon={isLocked ? "locked" : "unlocked"}
onClick={onLock}
>
{isLocked ? "Восстановить" : "Удалить"}
</MenuItemWithIcon> </MenuItemWithIcon>
</MenuButton> </MenuButton>
); );
@ -58,9 +64,9 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
return ( return (
<SeparatedMenu> <SeparatedMenu>
{canStar && ( {canStar && (
<Tippy content={isHeroic ? 'Убрать с главной' : 'На главную'}> <Tippy content={isHeroic ? "Убрать с главной" : "На главную"}>
<button className={className} onClick={onStar}> <button className={className} onClick={onStar}>
<Icon icon={isHeroic ? 'star_full' : 'star'} size={24} /> <Icon icon={isHeroic ? "star_full" : "star"} size={24} />
</button> </button>
</Tippy> </Tippy>
)} )}
@ -71,9 +77,9 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
</button> </button>
</Tippy> </Tippy>
<Tippy content={isLocked ? 'Восстановить' : 'Удалить'}> <Tippy content={isLocked ? "Восстановить" : "Удалить"}>
<button className={className} onClick={onLock}> <button className={className} onClick={onLock}>
<Icon icon={isLocked ? 'locked' : 'unlocked'} size={24} /> <Icon icon={isLocked ? "locked" : "unlocked"} size={24} />
</button> </button>
</Tippy> </Tippy>
</SeparatedMenu> </SeparatedMenu>

View 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 };

View file

@ -0,0 +1,30 @@
import { FC } from "react";
import { BorisContacts } from "~/components/boris/BorisContacts";
import { BorisStats } from "~/components/boris/BorisStats";
import { Group } from "~/components/containers/Group";
import { SuperPowersToggle } from "~/containers/auth/SuperPowersToggle";
import styles from "~/layouts/BorisLayout/styles.module.scss";
import { BorisUsageStats } from "~/types/boris";
interface Props {
isUser: boolean;
stats: BorisUsageStats;
isLoading: boolean;
}
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading }) => (
<Group className={styles.stats__container}>
<div className={styles.super_powers}>
<SuperPowersToggle />
</div>
<BorisContacts />
<div className={styles.stats__wrap}>
<BorisStats stats={stats} isLoading={isLoading} />
</div>
</Group>
);
export { BorisSidebar };

View file

@ -1,18 +1,18 @@
import React, { useEffect, useRef, VFC } from 'react'; import React, { useEffect, useRef, VFC } from "react";
import classNames from 'classnames'; import classNames from "classnames";
import { observer } from 'mobx-react-lite'; import { observer } from "mobx-react-lite";
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'; import PhotoSwipeUI_Default from "photoswipe/dist/photoswipe-ui-default.js";
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js'; import PhotoSwipeJs from "photoswipe/dist/photoswipe.js";
import { ImagePresets } from '~/constants/urls'; import { ImagePresets } from "~/constants/urls";
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useModal } from '~/hooks/modal/useModal'; import { useModal } from "~/hooks/modal/useModal";
import { IFile } from '~/types'; import { IFile } from "~/types";
import { DialogComponentProps } from '~/types/modal'; import { DialogComponentProps } from "~/types/modal";
import { getURL } from '~/utils/dom'; import { getURL } from "~/utils/dom";
import styles from './styles.module.scss'; import styles from "./styles.module.scss";
export interface PhotoSwipeProps extends DialogComponentProps { export interface PhotoSwipeProps extends DialogComponentProps {
items: IFile[]; items: IFile[];
@ -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);
@ -58,15 +61,21 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
}); });
ps.init(); ps.init();
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
<div className={classNames('pswp__bg', styles.bg)} /> className="pswp"
<div className={classNames('pswp__scroll-wrap', styles.wrap)}> tabIndex={-1}
role="dialog"
aria-hidden="true"
ref={ref}
>
<div className={classNames("pswp__bg", styles.bg)} />
<div className={classNames("pswp__scroll-wrap", styles.wrap)}>
<div className="pswp__container"> <div className="pswp__container">
<div className="pswp__item" /> <div className="pswp__item" />
<div className="pswp__item" /> <div className="pswp__item" />
@ -74,9 +83,12 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
</div> </div>
<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" />

View file

@ -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";
@ -49,6 +50,10 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
</VerticalMenu.Item> </VerticalMenu.Item>
</VerticalMenu> </VerticalMenu>
<div className={styles.toggles}>
<ProfileToggles />
</div>
<div className={styles.stats}> <div className={styles.stats}>
<ProfileStats /> <ProfileStats />
</div> </div>

View file

@ -19,3 +19,7 @@
.stats { .stats {
display: none; display: none;
} }
.toggles {
padding-top: $gap * 2;
}

View 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 };

View file

@ -8,6 +8,7 @@ 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 { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useSettings } from "~/utils/providers/SettingsProvider"; import { useSettings } from "~/utils/providers/SettingsProvider";
import { has } from "~/utils/ramda"; import { has } from "~/utils/ramda";
@ -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,7 +35,7 @@ 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}

View file

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

View file

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, VFC } from "react"; import React, { useCallback, useEffect, useMemo, VFC } from "react";
import { isNil } from "ramda"; import { isNil } from "ramda";
@ -9,6 +9,7 @@ import { SidebarStackCard } from "~/components/sidebar/SidebarStackCard";
import { SidebarName } from "~/constants/sidebar"; 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 { useAuth } from "~/hooks/auth/useAuth";
import type { SidebarComponentProps } from "~/types/sidebar"; import type { SidebarComponentProps } from "~/types/sidebar";
const tabs = ["profile", "bookmarks"] as const; const tabs = ["profile", "bookmarks"] as const;
@ -24,6 +25,8 @@ const ProfileSidebar: VFC<ProfileSidebarProps> = ({
page, page,
openSidebar, openSidebar,
}) => { }) => {
const { isUser } = useAuth();
const tab = useMemo( const tab = useMemo(
() => (page ? Math.max(tabs.indexOf(page), 0) : undefined), () => (page ? Math.max(tabs.indexOf(page), 0) : undefined),
[page], [page],
@ -38,6 +41,16 @@ const ProfileSidebar: VFC<ProfileSidebarProps> = ({
[open, onRequestClose], [open, onRequestClose],
); );
useEffect(() => {
if (!isUser) {
onRequestClose();
}
}, [isUser]);
if (!isUser) {
return null;
}
return ( return (
<SidebarWrapper onClose={onRequestClose}> <SidebarWrapper onClose={onRequestClose}>
<SidebarStack tab={tab} onTabChange={onTabChange}> <SidebarStack tab={tab} onTabChange={onTabChange}>

View 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]);
};

View file

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

View file

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

View file

@ -1,89 +1,85 @@
import { FC, useMemo } from 'react'; 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";
import { BorisUsageStats } from '~/types/boris'; import { BorisUsageStats } from "~/types/boris";
import { useAuthProvider } from '~/utils/providers/AuthProvider'; import { useAuthProvider } from "~/utils/providers/AuthProvider";
import styles from './styles.module.scss'; 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(
const commentsByMonth = useMemo(() => stats.backend.comments.by_month?.slice(0, -1), [ () => stats.backend.comments.by_month?.slice(0, -1),
stats.backend.comments.by_month, [stats.backend.comments.by_month],
]); );
const nodesByMonth = useMemo(() => stats.backend.nodes.by_month?.slice(0, -1), [ const nodesByMonth = useMemo(
stats.backend.comments.by_month, () => stats.backend.nodes.by_month?.slice(0, -1),
]); [stats.backend.comments.by_month],
);
return ( return (
<Container> <Container>
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.cover} /> <div className={styles.cover} />
<div className={styles.image}> <div className={styles.image}>
<div className={styles.caption}> <div className={styles.caption}>
<div className={styles.caption_text}>{title}</div> <div className={styles.caption_text}>{title}</div>
</div>
<img src="/images/boris_robot.svg" alt="Борис" />
</div> </div>
<div className={styles.container}> <img src="/images/boris_robot.svg" alt="Борис" />
<Card className={styles.content}>
<Group>
<Superpower>
<BorisSuperPowersSSR />
</Superpower>
<BorisGraphicStats
totalComments={stats.backend.comments.total}
commentsByMonth={commentsByMonth}
totalNodes={stats.backend.nodes.total}
nodesByMonth={nodesByMonth}
/>
<BorisComments />
</Group>
</Card>
<Group className={styles.stats}>
<Sticky>
<BorisSidebar
isTester={isTester}
stats={stats}
setBetaTester={setIsBetaTester}
isUser={isUser}
isLoading={isLoadingStats}
/>
</Sticky>
</Group>
</div>
</div> </div>
<SidebarRouter prefix="/" /> <div className={styles.container}>
</Container> <Card className={styles.content}>
); <Group>
} <Superpower>
); <BorisSuperPowersSSR />
</Superpower>
<BorisGraphicStats
totalComments={stats.backend.comments.total}
commentsByMonth={commentsByMonth}
totalNodes={stats.backend.nodes.total}
nodesByMonth={nodesByMonth}
/>
<BorisComments />
</Group>
</Card>
<Group className={styles.stats}>
<Sticky>
<BorisSidebar
stats={stats}
isUser={isUser}
isLoading={isLoadingStats}
/>
</Sticky>
</Group>
</div>
</div>
<SidebarRouter prefix="/" />
</Container>
);
});
export { BorisLayout }; export { BorisLayout };

View file

@ -1,16 +1,16 @@
import React, { VFC } from 'react'; import React, { VFC } from "react";
import { observer } from 'mobx-react-lite'; import { observer } from "mobx-react-lite";
import { PageTitle } from '~/components/common/PageTitle'; import { PageTitle } from "~/components/common/PageTitle";
import { useBoris } from '~/hooks/boris/useBoris'; import { useBoris } from "~/hooks/boris/useBoris";
import { useNodeComments } from '~/hooks/comments/useNodeComments'; import { useNodeComments } from "~/hooks/comments/useNodeComments";
import { useImageModal } from '~/hooks/navigation/useImageModal'; import { useImageModal } from "~/hooks/navigation/useImageModal";
import { useLoadNode } from '~/hooks/node/useLoadNode'; import { useLoadNode } from "~/hooks/node/useLoadNode";
import { BorisLayout } from '~/layouts/BorisLayout'; import { BorisLayout } from "~/layouts/BorisLayout";
import { CommentContextProvider } from '~/utils/context/CommentContextProvider'; import { CommentContextProvider } from "~/utils/context/CommentContextProvider";
import { NodeContextProvider } from '~/utils/context/NodeContextProvider'; import { NodeContextProvider } from "~/utils/context/NodeContextProvider";
import { getPageTitle } from '~/utils/ssr/getPageTitle'; import { getPageTitle } from "~/utils/ssr/getPageTitle";
const BorisPage: VFC = observer(() => { const BorisPage: VFC = observer(() => {
const { node, isLoading, update } = useLoadNode(696); const { node, isLoading, update } = useLoadNode(696);
@ -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}>
@ -39,12 +39,10 @@ const BorisPage: VFC = observer(() => {
onLoadMoreComments={onLoadMoreComments} onLoadMoreComments={onLoadMoreComments}
onDeleteComment={onDeleteComment} onDeleteComment={onDeleteComment}
> >
<PageTitle title={getPageTitle('Борис')} /> <PageTitle title={getPageTitle("Борис")} />
<BorisLayout <BorisLayout
title={title} title={title}
setIsBetaTester={setIsBetaTester}
isTester={isTester}
stats={stats} stats={stats}
isLoadingStats={isLoadingStats} isLoadingStats={isLoadingStats}
/> />

File diff suppressed because one or more lines are too long