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 {}
@ -17,31 +17,31 @@ interface Layer {
const layers: Layer[] = [
{
src: "/images/clouds__bg.svg",
src: '/images/clouds__bg.svg',
velocity: -0.3,
width: 3840,
height: 1080,
},
{
src: "/images/clouds__cube.svg",
src: '/images/clouds__cube.svg',
velocity: -0.1,
width: 3840,
height: 1080,
},
{
src: "/images/clouds__cloud.svg",
src: '/images/clouds__cloud.svg',
velocity: 0.2,
width: 3840,
height: 1080,
},
{
src: "/images/clouds__dudes.svg",
src: '/images/clouds__dudes.svg',
velocity: 0.5,
width: 3840,
height: 1080,
},
{
src: "/images/clouds__trash.svg",
src: '/images/clouds__trash.svg',
velocity: 0.8,
width: 3840,
height: 1080,
@ -52,7 +52,7 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
const containerRef = useRef<HTMLDivElement>(null);
const [loaded, setLoaded] = useState(false);
const imageRefs = useRef<Array<SVGImageElement | null>>([]);
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
const domRect = useRef<DOMRect>();
const onMouseMove = useCallback(
@ -84,11 +84,11 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
useEffect(() => {
const listener = throttle(100, onMouseMove);
document.addEventListener("mousemove", listener);
return () => document.removeEventListener("mousemove", listener);
document.addEventListener('mousemove', listener);
return () => document.removeEventListener('mousemove', listener);
}, []);
if (isMobile) {
if (isTablet) {
return null;
}
@ -103,16 +103,16 @@ const LoginScene: FC<LoginSceneProps> = memo(() => {
>
<defs>
<linearGradient id="fallbackGradient" x1={0} x2={0} y1={1} y2={0}>
<stop style={{ stopColor: "#ffccaa", stopOpacity: 1 }} offset="0" />
<stop style={{ stopColor: '#ffccaa', stopOpacity: 1 }} offset="0" />
<stop
style={{ stopColor: "#fff6d5", stopOpacity: 1 }}
style={{ stopColor: '#fff6d5', stopOpacity: 1 }}
offset="0.34655526"
/>
<stop
style={{ stopColor: "#afc6e9", stopOpacity: 1 }}
style={{ stopColor: '#afc6e9', stopOpacity: 1 }}
offset="0.765342"
/>
<stop style={{ stopColor: "#879fde", stopOpacity: 1 }} offset="1" />
<stop style={{ stopColor: '#879fde', stopOpacity: 1 }} offset="1" />
</linearGradient>
</defs>

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 {}
const Superpower: FC<IProps> = ({ children }) => {
const Superpower: FC<IProps> = observer(({ children }) => {
const { isTester } = useAuth();
if (!isTester) return null;
return <>{children}</>;
};
});
export { Superpower };

View file

@ -16,7 +16,11 @@ const Zone: FC<ZoneProps> = ({
children,
color = "normal",
}) => (
<div className={classNames(className, styles.pad, styles[color])}>
<div
className={classNames(className, styles.pad, styles[color], {
[styles.with_title]: !!title,
})}
>
{!!title && (
<div className={styles.title}>
<span>{title}</span>

View file

@ -8,7 +8,7 @@ $pad_usual: mix(white, $content_bg, 10%);
span {
position: absolute;
top: -5px;
top: -$gap;
left: $radius;
transform: translate(0, -100%);
background: $pad_usual;
@ -25,7 +25,7 @@ $pad_usual: mix(white, $content_bg, 10%);
}
.pad {
padding: $gap * 1.5 $gap $gap;
padding: $gap;
box-shadow: inset $pad_usual 0 0 0 2px;
border-radius: $radius;
position: relative;
@ -33,4 +33,8 @@ $pad_usual: mix(white, $content_bg, 10%);
&.danger {
box-shadow: inset $pad_danger 0 0 0 2px;
}
&.with_title {
padding-top: $gap * 2;
}
}

View file

@ -1,20 +1,20 @@
import React, { FC } from 'react';
import React, { FC } from "react";
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { EditorActionsPanel } from '~/components/editors/EditorActionsPanel';
import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
import { Filler } from "~/components/containers/Filler";
import { Group } from "~/components/containers/Group";
import { Padder } from "~/components/containers/Padder";
import { EditorActionsPanel } from "~/components/editors/EditorActionsPanel";
import { Button } from "~/components/input/Button";
import { InputText } from "~/components/input/InputText";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useNodeFormContext } from "~/hooks/node/useNodeFormFormik";
const EditorButtons: FC = () => {
const { values, handleChange, isSubmitting } = useNodeFormContext();
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
return (
<Padder style={{ position: 'relative' }}>
<Padder style={{ position: "relative" }}>
<EditorActionsPanel />
<Group horizontal>
@ -22,17 +22,17 @@ const EditorButtons: FC = () => {
<InputText
title="Название"
value={values.title}
handler={handleChange('title')}
autoFocus={!isMobile}
handler={handleChange("title")}
autoFocus={!isTablet}
maxLength={256}
disabled={isSubmitting}
/>
</Filler>
<Button
title={isMobile ? undefined : 'Сохранить'}
title={isTablet ? undefined : "Сохранить"}
iconRight="check"
color={values.is_promoted ? 'primary' : 'lab'}
color={values.is_promoted ? "primary" : "lab"}
disabled={isSubmitting}
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 { useWindowSize } from '~/hooks/dom/useWindowSize';
import { UploadStatus } from '~/store/uploader/UploaderStore';
import { IFile } from '~/types';
import { SortableImageGrid } from "~/components/sortable";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { UploadStatus } from "~/store/uploader/UploaderStore";
import { IFile } from "~/types";
interface IProps {
files: IFile[];
@ -12,20 +12,20 @@ interface IProps {
}
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
const onMove = useCallback(
(newFiles: IFile[]) => {
setFiles(newFiles.filter(it => it));
},
[setFiles, files]
[setFiles, files],
);
const onDrop = useCallback(
(id: IFile['id']) => {
(id: IFile["id"]) => {
setFiles(files.filter(file => file && file.id !== id));
},
[setFiles, files]
[setFiles, files],
);
return (
@ -34,7 +34,7 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
onSortEnd={onMove}
items={files}
locked={locked}
size={!isMobile ? 220 : (innerWidth - 60) / 2}
size={!isTablet ? 220 : (innerWidth - 60) / 2}
/>
);
};

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 { MenuDots } from '~/components/common/MenuDots';
import { CellShade } from '~/components/flow/CellShade';
import { FlowCellImage } from '~/components/flow/FlowCellImage';
import { FlowCellMenu } from '~/components/flow/FlowCellMenu';
import { FlowCellText } from '~/components/flow/FlowCellText';
import { useClickOutsideFocus } from '~/hooks/dom/useClickOutsideFocus';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls';
import { FlowDisplay, INode } from '~/types';
import { Anchor } from "~/components/common/Anchor";
import { MenuDots } from "~/components/common/MenuDots";
import { CellShade } from "~/components/flow/CellShade";
import { FlowCellImage } from "~/components/flow/FlowCellImage";
import { FlowCellMenu } from "~/components/flow/FlowCellMenu";
import { FlowCellText } from "~/components/flow/FlowCellText";
import { useClickOutsideFocus } from "~/hooks/dom/useClickOutsideFocus";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useFlowCellControls } from "~/hooks/flow/useFlowCellControls";
import { FlowDisplay, INode } from "~/types";
import styles from './styles.module.scss';
import styles from "./styles.module.scss";
interface Props {
id: INode['id'];
id: INode["id"];
to: string;
title: string;
image?: string;
@ -25,7 +25,7 @@ interface Props {
text?: string;
flow: FlowDisplay;
canEdit?: boolean;
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void;
onChangeCellView: (id: INode["id"], flow: FlowDisplay) => void;
}
const FlowCell: FC<Props> = ({
@ -39,10 +39,12 @@ const FlowCell: FC<Props> = ({
canEdit = false,
onChangeCellView,
}) => {
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
const withText =
((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text;
((!!flow.display && flow.display !== "single") || !image) &&
flow.show_description &&
!!text;
const {
hasDescription,
setViewHorizontal,
@ -51,21 +53,26 @@ const FlowCell: FC<Props> = ({
setViewSingle,
toggleViewDescription,
} = useFlowCellControls(id, text, flow, onChangeCellView);
const { isActive: isMenuActive, activate, ref, deactivate } = useClickOutsideFocus();
const {
isActive: isMenuActive,
activate,
ref,
deactivate,
} = useClickOutsideFocus();
const shadeSize = useMemo(() => {
const min = isMobile ? 10 : 15;
const max = isMobile ? 20 : 40;
const min = isTablet ? 10 : 15;
const max = isTablet ? 20 : 40;
return withText ? min : max;
}, [withText, isMobile]);
}, [withText, isTablet]);
const shadeAngle = useMemo(() => {
if (flow.display === 'vertical') {
if (flow.display === "vertical") {
return 9;
}
if (flow.display === 'horizontal') {
if (flow.display === "horizontal") {
return 15;
}
@ -73,7 +80,10 @@ const FlowCell: FC<Props> = ({
}, [flow.display]);
return (
<div className={classNames(styles.cell, styles[flow.display || 'single'])} ref={ref as any}>
<div
className={classNames(styles.cell, styles[flow.display || "single"])}
ref={ref as any}
>
{canEdit && !isMenuActive && (
<div className={styles.menu}>
<MenuDots onClick={activate} />
@ -98,7 +108,10 @@ const FlowCell: FC<Props> = ({
<Anchor className={styles.link} href={to}>
{withText && (
<FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
<FlowCellText
className={styles.text}
heading={<h4 className={styles.title}>{title}</h4>}
>
{text!}
</FlowCellText>
)}
@ -113,7 +126,12 @@ const FlowCell: FC<Props> = ({
)}
{!!title && (
<CellShade color={color} className={styles.shade} size={shadeSize} angle={shadeAngle} />
<CellShade
color={color}
className={styles.shade}
size={shadeSize}
angle={shadeAngle}
/>
)}
{!withText && (

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 SwiperCore, { Autoplay, EffectFade, Lazy, Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperClass from 'swiper/types/swiper-class';
import classNames from "classnames";
import SwiperCore, { Autoplay, EffectFade, Lazy, Navigation } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import SwiperClass from "swiper/types/swiper-class";
import { Icon } from '~/components/input/Icon';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { ImagePresets, URLS } from '~/constants/urls';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useNavigation } from '~/hooks/navigation/useNavigation';
import { IFlowNode } from '~/types';
import { getURLFromString } from '~/utils/dom';
import { Icon } from "~/components/input/Icon";
import { LoaderCircle } from "~/components/input/LoaderCircle";
import { ImagePresets, URLS } from "~/constants/urls";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useNavigation } from "~/hooks/navigation/useNavigation";
import { IFlowNode } from "~/types";
import { getURLFromString } from "~/utils/dom";
import styles from './styles.module.scss';
import styles from "./styles.module.scss";
SwiperCore.use([EffectFade, Lazy, Autoplay, Navigation]);
@ -34,14 +34,17 @@ const lazy = {
};
export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
const { push } = useNavigation();
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
const [controlledSwiper, setControlledSwiper] = useState<
SwiperClass | undefined
>(undefined);
const [currentIndex, setCurrentIndex] = useState(heroes.length);
const preset = useMemo(() => (isMobile ? ImagePresets.cover : ImagePresets.small_hero), [
isMobile,
]);
const preset = useMemo(
() => (isTablet ? ImagePresets.cover : ImagePresets.small_hero),
[isTablet],
);
const onNext = useCallback(() => {
controlledSwiper?.slideNext(1);
@ -79,7 +82,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
(sw: SwiperClass) => {
push(URLS.NODE_URL(heroes[sw.realIndex]?.id));
},
[push, heroes]
[push, heroes],
);
if (!heroes.length) {
@ -135,7 +138,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
<img
src={getURLFromString(node.thumbnail!, preset)}
alt=""
className={classNames(styles.preview, 'swiper-lazy')}
className={classNames(styles.preview, "swiper-lazy")}
/>
</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 { MenuButton, MenuItemWithIcon, SeparatedMenu } from '~/components/menu';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { Icon } from "~/components/input/Icon";
import { MenuButton, MenuItemWithIcon, SeparatedMenu } from "~/components/menu";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import styles from './styles.module.scss';
import styles from "./styles.module.scss";
interface NodeEditMenuProps {
className?: string;
@ -30,17 +30,20 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
onLock,
onEdit,
}) => {
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
if (isMobile) {
if (isTablet) {
return (
<MenuButton
icon={<Icon icon="dots-vertical" className={styles.icon} size={24} />}
className={className}
>
{canStar && (
<MenuItemWithIcon icon={isHeroic ? 'star_full' : 'star'} onClick={onStar}>
{isHeroic ? 'Убрать с главной' : 'На главную'}
<MenuItemWithIcon
icon={isHeroic ? "star_full" : "star"}
onClick={onStar}
>
{isHeroic ? "Убрать с главной" : "На главную"}
</MenuItemWithIcon>
)}
@ -48,8 +51,11 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
Редактировать
</MenuItemWithIcon>
<MenuItemWithIcon icon={isLocked ? 'locked' : 'unlocked'} onClick={onLock}>
{isLocked ? 'Восстановить' : 'Удалить'}
<MenuItemWithIcon
icon={isLocked ? "locked" : "unlocked"}
onClick={onLock}
>
{isLocked ? "Восстановить" : "Удалить"}
</MenuItemWithIcon>
</MenuButton>
);
@ -58,9 +64,9 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
return (
<SeparatedMenu>
{canStar && (
<Tippy content={isHeroic ? 'Убрать с главной' : 'На главную'}>
<Tippy content={isHeroic ? "Убрать с главной" : "На главную"}>
<button className={className} onClick={onStar}>
<Icon icon={isHeroic ? 'star_full' : 'star'} size={24} />
<Icon icon={isHeroic ? "star_full" : "star"} size={24} />
</button>
</Tippy>
)}
@ -71,9 +77,9 @@ const NodeEditMenu: VFC<NodeEditMenuProps> = ({
</button>
</Tippy>
<Tippy content={isLocked ? 'Восстановить' : 'Удалить'}>
<Tippy content={isLocked ? "Восстановить" : "Удалить"}>
<button className={className} onClick={onLock}>
<Icon icon={isLocked ? 'locked' : 'unlocked'} size={24} />
<Icon icon={isLocked ? "locked" : "unlocked"} size={24} />
</button>
</Tippy>
</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 { observer } from 'mobx-react-lite';
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js';
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js';
import classNames from "classnames";
import { observer } from "mobx-react-lite";
import PhotoSwipeUI_Default from "photoswipe/dist/photoswipe-ui-default.js";
import PhotoSwipeJs from "photoswipe/dist/photoswipe.js";
import { ImagePresets } from '~/constants/urls';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useModal } from '~/hooks/modal/useModal';
import { IFile } from '~/types';
import { DialogComponentProps } from '~/types/modal';
import { getURL } from '~/utils/dom';
import { ImagePresets } from "~/constants/urls";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useModal } from "~/hooks/modal/useModal";
import { IFile } from "~/types";
import { DialogComponentProps } from "~/types/modal";
import { getURL } from "~/utils/dom";
import styles from './styles.module.scss';
import styles from "./styles.module.scss";
export interface PhotoSwipeProps extends DialogComponentProps {
items: IFile[];
@ -22,7 +22,7 @@ export interface PhotoSwipeProps extends DialogComponentProps {
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
let ref = useRef<HTMLDivElement>(null);
const { hideModal } = useModal();
const { isMobile } = useWindowSize();
const { isTablet } = useWindowSize();
useEffect(() => {
new Promise(async resolve => {
@ -34,7 +34,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
img.onload = () => {
resolveImage({
src: getURL(image, isMobile ? ImagePresets[900] : ImagePresets[1600]),
src: getURL(
image,
isTablet ? ImagePresets[900] : ImagePresets[1600],
),
h: img.naturalHeight,
w: img.naturalWidth,
});
@ -45,8 +48,8 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
};
img.src = getURL(image, ImagePresets[1600]);
})
)
}),
),
);
resolve(images);
@ -58,15 +61,21 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
});
ps.init();
ps.listen('destroy', hideModal);
ps.listen('close', hideModal);
ps.listen("destroy", hideModal);
ps.listen("close", hideModal);
});
}, [hideModal, items, index, isMobile]);
}, [hideModal, items, index, isTablet]);
return (
<div className="pswp" 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"
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__item" />
<div className="pswp__item" />
@ -74,9 +83,12 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
</div>
<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" />
<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__icn">
@ -96,7 +108,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
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__center" />

View file

@ -15,6 +15,7 @@ import { useAuth } from "~/hooks/auth/useAuth";
import markdown from "~/styles/common/markdown.module.scss";
import { ProfileSidebarLogoutButton } from "../ProfileSidebarLogoutButton";
import { ProfileToggles } from "../ProfileToggles";
import styles from "./styles.module.scss";
@ -49,6 +50,10 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
</VerticalMenu.Item>
</VerticalMenu>
<div className={styles.toggles}>
<ProfileToggles />
</div>
<div className={styles.stats}>
<ProfileStats />
</div>

View file

@ -19,3 +19,7 @@
.stats {
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 { ERROR_LITERAL } from "~/constants/errors";
import { ProfileAccounts } from "~/containers/profile/ProfileAccounts";
import { useWindowSize } from "~/hooks/dom/useWindowSize";
import { useSettings } from "~/utils/providers/SettingsProvider";
import { has } from "~/utils/ramda";
@ -20,10 +21,11 @@ const getError = (error?: string) =>
const UserSettingsView: FC<UserSettingsViewProps> = () => {
const { values, handleChange, errors } = useSettings();
const { isPhone } = useWindowSize();
return (
<Group>
<Group horizontal className={styles.base_info}>
<Group horizontal={!isPhone} className={styles.base_info}>
<Superpower>
<Zone className={styles.avatar} title="Фото">
<small>
@ -33,7 +35,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
</Zone>
</Superpower>
<Zone title="О себе">
<Zone title="О себе" className={styles.about}>
<Group>
<InputText
value={values.fullname}

View file

@ -3,6 +3,10 @@
$pad_danger: mix($red, $content_bg, 70%);
$pad_usual: mix(white, $content_bg, 10%);
.about {
flex: 4;
}
.wrap {
padding: $gap;
z-index: 4;
@ -21,5 +25,5 @@ div.base_info.base_info {
}
.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";
@ -9,6 +9,7 @@ import { SidebarStackCard } from "~/components/sidebar/SidebarStackCard";
import { SidebarName } from "~/constants/sidebar";
import { ProfileSidebarMenu } from "~/containers/profile/ProfileSidebarMenu";
import { SidebarWrapper } from "~/containers/sidebars/SidebarWrapper";
import { useAuth } from "~/hooks/auth/useAuth";
import type { SidebarComponentProps } from "~/types/sidebar";
const tabs = ["profile", "bookmarks"] as const;
@ -24,6 +25,8 @@ const ProfileSidebar: VFC<ProfileSidebarProps> = ({
page,
openSidebar,
}) => {
const { isUser } = useAuth();
const tab = useMemo(
() => (page ? Math.max(tabs.indexOf(page), 0) : undefined),
[page],
@ -38,6 +41,16 @@ const ProfileSidebar: VFC<ProfileSidebarProps> = ({
[open, onRequestClose],
);
useEffect(() => {
if (!isUser) {
onRequestClose();
}
}, [isUser]);
if (!isUser) {
return null;
}
return (
<SidebarWrapper onClose={onRequestClose}>
<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 { useAuth } from '~/hooks/auth/useAuth';
import { useLastSeenBoris } from '~/hooks/auth/useLastSeenBoris';
import { useBorisStats } from '~/hooks/boris/useBorisStats';
import { IComment } from '~/types';
import { useRandomPhrase } from "~/constants/phrases";
import { useLastSeenBoris } from "~/hooks/auth/useLastSeenBoris";
import { useBorisStats } from "~/hooks/boris/useBorisStats";
import { IComment } from "~/types";
export const useBoris = (comments: IComment[]) => {
const title = useRandomPhrase('BORIS_TITLE');
const title = useRandomPhrase("BORIS_TITLE");
const { lastSeen, setLastSeen } = useLastSeenBoris();
const { isTester, setIsTester } = useAuth();
useEffect(() => {
const last_comment = comments[0];
@ -32,12 +30,5 @@ export const useBoris = (comments: IComment[]) => {
const { stats, isLoading: isLoadingStats } = useBorisStats();
const setIsBetaTester = useCallback(
(isTester: boolean) => {
setIsTester(isTester);
},
[setIsTester]
);
return { setIsBetaTester, isTester, stats, title, isLoadingStats };
return { stats, title, isLoadingStats };
};

View file

@ -1,24 +1,30 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from "react";
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(
() =>
setSize({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
isMobile: window.innerWidth < 768,
isTablet: window.innerWidth < 768,
isPhone: window.innerWidth < 500,
}),
[]
[],
);
useEffect(() => {
onResize();
window.addEventListener('resize', onResize);
window.addEventListener("resize", onResize);
return () => window.removeEventListener('resize', onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
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 { BorisSidebar } from '~/components/boris/BorisSidebar';
import { Superpower } from '~/components/boris/Superpower';
import { Card } from '~/components/containers/Card';
import { Group } from '~/components/containers/Group';
import { Sticky } from '~/components/containers/Sticky';
import { BorisComments } from '~/containers/boris/BorisComments';
import { BorisSuperPowersSSR } from '~/containers/boris/BorisSuperpowers/ssr';
import { Container } from '~/containers/main/Container';
import { SidebarRouter } from '~/containers/main/SidebarRouter';
import { BorisUsageStats } from '~/types/boris';
import { useAuthProvider } from '~/utils/providers/AuthProvider';
import { BorisGraphicStats } from "~/components/boris/BorisGraphicStats";
import { Superpower } from "~/components/boris/Superpower";
import { Card } from "~/components/containers/Card";
import { Group } from "~/components/containers/Group";
import { Sticky } from "~/components/containers/Sticky";
import { BorisComments } from "~/containers/boris/BorisComments";
import { BorisSidebar } from "~/containers/boris/BorisSidebar";
import { BorisSuperPowersSSR } from "~/containers/boris/BorisSuperpowers/ssr";
import { Container } from "~/containers/main/Container";
import { SidebarRouter } from "~/containers/main/SidebarRouter";
import { BorisUsageStats } from "~/types/boris";
import { useAuthProvider } from "~/utils/providers/AuthProvider";
import styles from './styles.module.scss';
import styles from "./styles.module.scss";
type IProps = {
title: string;
setIsBetaTester: (val: boolean) => void;
isTester: boolean;
stats: BorisUsageStats;
isLoadingStats: boolean;
};
const BorisLayout: FC<IProps> = observer(
({ title, setIsBetaTester, isTester, stats, isLoadingStats }) => {
const { isUser } = useAuthProvider();
const commentsByMonth = useMemo(() => 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 BorisLayout: FC<IProps> = observer(({ title, stats, isLoadingStats }) => {
const { isUser } = useAuthProvider();
const commentsByMonth = useMemo(
() => 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],
);
return (
<Container>
<div className={styles.wrap}>
<div className={styles.cover} />
return (
<Container>
<div className={styles.wrap}>
<div className={styles.cover} />
<div className={styles.image}>
<div className={styles.caption}>
<div className={styles.caption_text}>{title}</div>
</div>
<img src="/images/boris_robot.svg" alt="Борис" />
<div className={styles.image}>
<div className={styles.caption}>
<div className={styles.caption_text}>{title}</div>
</div>
<div className={styles.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
isTester={isTester}
stats={stats}
setBetaTester={setIsBetaTester}
isUser={isUser}
isLoading={isLoadingStats}
/>
</Sticky>
</Group>
</div>
<img src="/images/boris_robot.svg" alt="Борис" />
</div>
<SidebarRouter prefix="/" />
</Container>
);
}
);
<div className={styles.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 };

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

File diff suppressed because one or more lines are too long