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

refactor editos

This commit is contained in:
Fedor Katurov 2023-11-20 22:21:08 +06:00
parent 03ddb1862c
commit 5e9c111e0f
149 changed files with 416 additions and 317 deletions

View file

@ -0,0 +1,76 @@
import React, { FC, MouseEventHandler, useEffect, useRef } from 'react';
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
import { Icon } from '~/components/input/Icon';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import styles from './styles.module.scss';
interface IProps {
children: React.ReactChild;
header?: JSX.Element;
footer?: JSX.Element;
backdrop?: JSX.Element;
size?: 'medium' | 'big';
width?: number;
error?: string;
is_loading?: boolean;
overlay?: JSX.Element;
onOverlayClick?: MouseEventHandler<HTMLDivElement>;
onRefCapture?: (ref: any) => void;
onClose?: () => void;
}
const BetterScrollDialog: FC<IProps> = ({
children,
header,
footer,
backdrop,
width = 600,
error,
onClose,
is_loading,
overlay = null,
}) => {
const ref = useRef(null);
useEffect(() => {
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
return () => clearAllBodyScrollLocks();
}, [ref]);
return (
<div className={styles.wrap} ref={ref}>
{backdrop && <div className={styles.backdrop}>{backdrop}</div>}
<div className={styles.container} style={{ maxWidth: width }}>
{onClose && (
<div className={styles.close} onClick={onClose}>
<Icon icon="close" />
</div>
)}
{header && <div className={styles.header}>{header}</div>}
<div className={styles.body}>
{children}
{error && <div className={styles.error}>{error}</div>}
</div>
{!!is_loading && (
<div className={styles.shade}>
<LoaderCircle size={48} />
</div>
)}
{overlay}
{footer && <div className={styles.footer}>{footer}</div>}
</div>
</div>
);
};
export { BetterScrollDialog };

View file

@ -0,0 +1,150 @@
@import 'src/styles/variables';
.wrap {
width: 100vw;
height: 100vh;
background: $content_bg_backdrop;
display: flex;
align-items: center;
justify-content: center;
padding: 70px 20px 40px 20px;
box-sizing: border-box;
@include tablet {
padding: 20px 0 0 0;
}
}
.container {
display: flex;
align-items: stretch;
justify-content: center;
flex-direction: column;
min-width: $cell;
max-width: 400px;
max-height: calc(100vh - 75px);
width: 100%;
position: relative;
box-sizing: border-box;
& > div:nth-child(2) {
border-top-left-radius: $dialog_radius;
border-top-right-radius: $dialog_radius;
}
& > div:last-child {
border-bottom-left-radius: $dialog_radius;
border-bottom-right-radius: $dialog_radius;
}
}
.header,
.footer {
@include outer_shadow();
background: $content_bg_dark;
}
.body {
@include outer_shadow();
position: relative;
overflow: auto;
flex: 1;
background: $content_bg;
}
@keyframes appear {
0% {
top: -48px;
}
100% {
top: -58px;
}
}
.close {
@include outer_shadow;
background: $content_bg_lighter;
width: 36px;
height: 36px;
position: absolute;
top: -14px;
right: 4px;
transform: translate(50%, 0) scale(1);
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
cursor: pointer;
transition: transform 0.25s, background-color 0.25s;
animation: appear 0.5s forwards;
z-index: 10;
@include tablet {
top: -16px;
right: 16px;
}
&:hover {
background-color: $color_danger;
transform: translate(50%, 0) scale(1.25);
}
svg {
width: 20px;
height: 20px;
}
}
.error {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
pointer-events: none;
background: linear-gradient(0deg, $color_danger 50%, transparent);
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 0 $radius $radius;
z-index: 11;
}
.backdrop {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.shade {
position: absolute;
background: $content_bg_backdrop;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius;
animation: appear 1s forwards;
svg {
fill: white;
}
}

View file

@ -0,0 +1,11 @@
import React, { AllHTMLAttributes, FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = AllHTMLAttributes<HTMLDivElement> & { is_blurred: boolean };
export const BlurWrapper: FC<IProps> = ({ children, is_blurred }) => (
<div className={classNames(styles.blur, { [styles.is_blurred]: is_blurred })}>{children}</div>
);

View file

@ -0,0 +1,9 @@
@import "src/styles/variables";
.blur {
padding-top: $header_height + 2px;
display: flex;
box-sizing: border-box;
flex-direction: column;
min-height: 100vh;
}

View file

@ -0,0 +1,34 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import { DivProps } from '~/utils/types';
import styles from './styles.module.scss';
export type CardProps = DivProps & {
seamless?: boolean;
elevation?: -1 | 0 | 1;
};
const Card: FC<CardProps> = ({
className,
children,
seamless,
elevation = 1,
...props
}) => (
<div
className={classNames(
styles.card,
{ seamless },
styles[`elevation-${elevation}`],
className,
)}
{...props}
>
{children}
</div>
);
export { Card };

View file

@ -0,0 +1,24 @@
@import 'src/styles/variables';
.card {
background-color: $content_bg;
border-radius: $panel_radius;
padding: $gap;
&.elevation--1 {
@include inner_shadow;
background: linear-gradient(135deg, $content_bg_dark, $content_bg);
}
&.elevation-1 {
@include outer_shadow();
}
&.elevation-0 {
background: $content_bg_light;
}
&:global(.seamless) {
padding: 0;
}
}

View file

@ -0,0 +1,22 @@
import React, { FC, HTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = HTMLAttributes<HTMLDivElement> & {
children: any;
size: number;
};
const CellGrid: FC<IProps> = ({ children, size, className, ...props }) => (
<div
className={classNames(styles.grid, className)}
style={{ gridTemplateColumns: `repeat(auto-fit, minmax(${size}px, 1fr))` }}
{...props}
>
{children}
</div>
);
export { CellGrid };

View file

@ -0,0 +1,8 @@
@import "src/styles/variables";
.grid {
display: grid;
grid-auto-rows: 1fr;
grid-column-gap: $gap;
grid-row-gap: $gap;
}

View file

@ -0,0 +1,55 @@
import React, { FC, useEffect, useRef, useState } from 'react';
import Masonry from 'react-masonry-css';
import { useScrollEnd } from '~/hooks/dom/useScrollEnd';
import styles from './styles.module.scss';
const defaultColumns = {
default: 2,
1280: 1,
};
interface ColumnsProps {
cols?: Record<number, number>;
onScrollEnd?: () => void;
hasMore?: boolean;
}
const Columns: FC<ColumnsProps> = ({
children,
cols = defaultColumns,
onScrollEnd,
hasMore,
}) => {
const ref = useRef<HTMLDivElement>(null);
const [columns, setColumns] = useState<Element[]>([]);
useEffect(() => {
const childs = ref.current?.querySelectorAll(`.${styles.column}`);
if (!childs) return;
const timeout = setTimeout(() => setColumns([...childs]), 150);
return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref.current]);
useScrollEnd(columns, onScrollEnd, { active: hasMore, threshold: 2 });
return (
<div ref={ref}>
<Masonry
className={styles.wrap}
breakpointCols={cols}
columnClassName={styles.column}
>
{children}
</Masonry>
</div>
);
};
export { Columns };

View file

@ -0,0 +1,28 @@
@import 'src/styles/variables';
@import 'src/styles/mixins';
div.wrap {
display: flex;
width: 100%;
margin-right: 0;
padding: $gap $gap * 0.5;
align-items: flex-start;
@include tablet {
padding: 0 $gap * 0.5;
}
}
.column {
background-clip: padding-box;
box-sizing: border-box;
padding: 0 $gap * 0.5;
@include tablet {
padding: 0;
}
& > div {
margin-bottom: $gap;
}
}

View file

@ -0,0 +1,47 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { imagePresets } from '~/constants/urls';
import { IUser } from '~/types/auth';
import { getURL } from '~/utils/dom';
import styles from './styles.module.scss';
interface IProps {
cover: IUser['cover'];
}
const CoverBackdrop: FC<IProps> = ({ cover }) => {
const ref = useRef<HTMLImageElement>(null);
const [is_loaded, setIsLoaded] = useState(false);
const onLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]);
const image = getURL(cover, imagePresets.cover);
useEffect(() => {
if (!cover || !cover.url || !ref || !ref.current) return;
ref.current.src = '';
setIsLoaded(false);
ref.current.src = getURL(cover, imagePresets.cover);
}, [cover]);
if (!cover) return null;
return (
<div
className={classNames(styles.cover, { [styles.active]: is_loaded })}
style={{ backgroundImage: `url("${image}")` }}
>
{
// TODO: use ImageWithSSRLoad here if you will face any bugs
}
<img onLoad={onLoad} ref={ref} alt="" />
</div>
);
};
export { CoverBackdrop };

View file

@ -0,0 +1,44 @@
@import 'src/styles/variables';
.cover {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: 50% 50% no-repeat;
background-size: cover;
opacity: 0;
transition: opacity 1s;
&.active {
opacity: 1;
}
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: $content_bg_backdrop;
}
&::before {
content: '';
background: url('../../../sprites/stripes.svg');
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0.5;
}
img {
width: 0;
height: 0;
opacity: 0;
}
}

View file

@ -0,0 +1,11 @@
import React, { FC, ReactNode } from 'react';
import styles from './styles.module.scss';
interface IProps {
children: ReactNode;
}
const DialogTitle: FC<IProps> = ({ children }) => <h2 className={styles.title}>{children}</h2>;
export { DialogTitle };

View file

@ -0,0 +1,6 @@
@import "src/styles/variables";
.title {
margin: $gap 0 $gap * 4 !important;
text-transform: uppercase;
}

View file

@ -0,0 +1,11 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = React.HTMLAttributes<HTMLDivElement>;
export const Filler: FC<IProps> = ({ className = '', ...props }) => (
<div className={classNames(styles.filler, className)} {...props} />
);

View file

@ -0,0 +1,5 @@
@import "src/styles/variables";
.filler {
flex: 1;
}

View file

@ -0,0 +1,56 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = React.HTMLAttributes<HTMLDivElement> & {
horizontal?: boolean;
vertical?: boolean;
columns?: string;
rows?: string;
size?: string;
square?: boolean;
gap?: number;
stretchy?: boolean;
};
const Grid: FC<IProps> = ({
children,
className = '',
horizontal = false,
vertical = false,
square = false,
size = 'auto',
style = {},
columns = 'auto',
rows = 'auto',
gap = 10,
stretchy,
...props
}) => (
<div
className={classNames(styles.grid, className, {
[styles.horizontal]: horizontal,
[styles.vertical]: !horizontal,
[styles.square]: square,
[styles.stretchy]: stretchy,
})}
style={{
...style,
gridTemplateColumns: square
? `repeat(auto-fill, ${(columns !== 'auto' && columns) || size})`
: columns,
gridTemplateRows: square ? `repeat(auto-fill, ${(rows !== 'auto' && rows) || size})` : rows,
gridAutoRows: rows,
gridAutoColumns: columns,
gridRowGap: gap,
gridColumnGap: gap,
}}
{...props}
>
{children}
</div>
);
export { Grid };

View file

@ -0,0 +1,24 @@
@import "src/styles/variables";
.grid {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-row-gap: $gap;
grid-column-gap: $gap;
grid-auto-flow: row;
grid-auto-rows: auto;
grid-auto-columns: auto;
&.horizontal {
grid-auto-flow: column;
}
&.square {
grid-auto-flow: dense;
}
&.stretchy {
flex: 1;
}
}

View file

@ -0,0 +1,44 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = React.HTMLAttributes<HTMLDivElement> & {
horizontal?: boolean;
top?: boolean;
bottom?: boolean;
wrap?: boolean;
seamless?: boolean;
};
const Group: FC<IProps> = ({
children,
className = '',
horizontal = false,
top = false,
bottom = false,
wrap = false,
seamless = false,
...props
}) => (
<div
className={classNames(
styles.group,
{
[styles.horizontal]: horizontal,
[styles.vertical]: !horizontal,
[styles.top]: top,
[styles.bottom]: bottom,
[styles.wrap]: wrap,
[styles.seamless]: seamless,
},
className
)}
{...props}
>
{children}
</div>
);
export { Group };

View file

@ -0,0 +1,49 @@
@import "src/styles/variables";
.group {
display: flex;
flex: 1;
> :global(.group_filler) {
flex: 1;
}
&.vertical {
flex-direction: column;
& > * {
margin: $gap*0.5 0;
&:first-child { margin-top: 0; }
&:last-child { margin-bottom: 0; }
}
}
&.horizontal {
flex-direction: row;
align-items: center;
&.top {
align-items: flex-start;
}
&.bottom {
align-items: flex-end;
}
& > * {
margin: 0 $gap * 0.5;
&:first-child { margin-left: 0; }
&:last-child { margin-right: 0; }
}
}
&.wrap {
flex-wrap: wrap;
}
&.seamless > * {
margin: 0;
}
}

View file

@ -0,0 +1,44 @@
import React, { FC, HTMLAttributes, useCallback, useEffect, useRef } from 'react';
import styles from './styles.module.scss';
interface IProps extends HTMLAttributes<HTMLDivElement> {
hasMore: boolean;
scrollReactPx?: number;
loadMore: () => void;
}
const InfiniteScroll: FC<IProps> = ({ children, hasMore, scrollReactPx, loadMore, ...props }) => {
const ref = useRef<HTMLDivElement>(null);
const onScrollEnd = useCallback(
(entries: IntersectionObserverEntry[]) => {
if (!hasMore || !entries[0].isIntersecting) return;
loadMore();
},
[hasMore, loadMore]
);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(onScrollEnd, {
root: null,
rootMargin: '200px',
threshold: 1.0,
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [onScrollEnd]);
return (
<div {...props}>
{children}
{hasMore && <div className={styles.more} ref={ref} />}
</div>
);
};
export { InfiniteScroll };

View file

@ -0,0 +1,4 @@
@import "src/styles/variables";
.more {
}

View file

@ -0,0 +1,21 @@
import React, { DetailedHTMLProps, VFC, HTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from '~/styles/common/markdown.module.scss';
import { formatText } from '~/utils/dom';
interface IProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
children?: string;
}
const Markdown: VFC<IProps> = ({ className, children = '', ...props }) => (
<div
className={classNames(styles.wrapper, className)}
{...props}
dangerouslySetInnerHTML={{ __html: formatText(children) }}
/>
);
export { Markdown };

View file

@ -0,0 +1,21 @@
import React, { FC, MouseEventHandler } from 'react';
import ReactDOM from 'react-dom';
import styles from './styles.module.scss';
type IProps = {
onOverlayClick: MouseEventHandler;
};
const ModalWrapper: FC<IProps> = ({ children, onOverlayClick }) => {
return ReactDOM.createPortal(
<div className={styles.fixed}>
<div className={styles.overlay} onClick={onOverlayClick} />
<div className={styles.content}>{children}</div>
</div>,
document.body
);
};
export { ModalWrapper };

View file

@ -0,0 +1,58 @@
@import 'src/styles/variables';
.fixed {
position: fixed;
z-index: 30;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.content {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
animation: appear 0.25s forwards;
}
.content_scroller {
width: 100%;
max-width: 100vw;
max-height: 100vh;
overflow: auto;
}
.content_padder {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
.overlay {
@include blur;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
animation: appear 0.25s forwards;
}

View file

@ -0,0 +1,31 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = React.HTMLAttributes<HTMLDivElement> & {
padding?: number;
vertical?: boolean;
horizontal?: boolean;
};
const Padder: FC<IProps> = ({
padding,
children,
className,
style = {},
vertical,
horizontal,
...props
}) => (
<div
className={classNames(styles.padder, className, { vertical, horizontal })}
style={padding ? { ...style, padding } : style}
{...props}
>
{children}
</div>
);
export { Padder };

View file

@ -0,0 +1,15 @@
@import "src/styles/variables";
.padder {
padding: $gap;
&:global(.horizontal) {
padding-top: 0;
padding-bottom: 0;
}
&:global(.vertical) {
padding-left: 0;
padding-right: 0;
}
}

View file

@ -0,0 +1,18 @@
import React, { FC, HTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
type IProps = HTMLAttributes<HTMLDivElement> & {
seamless?: boolean;
stretchy?: boolean;
};
const Panel: FC<IProps> = ({ className, children, seamless, stretchy, ...props }) => (
<div className={classNames(styles.panel, className, { seamless, stretchy })} {...props}>
{children}
</div>
);
export { Panel };

View file

@ -0,0 +1,16 @@
@import "src/styles/variables";
.panel {
padding: $gap;
min-height: $input_height + $gap;
display: flex;
align-items: center;
justify-content: stretch;
box-sizing: border-box;
flex-direction: row;
@include outer_shadow();
&:global(.seamless) { padding: 0; }
&:global(.stretchy) { flex: 1; align-items: flex-start; }
}

View file

@ -0,0 +1,15 @@
import React, { DetailsHTMLAttributes, FC } from 'react';
import StickyBox from 'react-sticky-box';
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {
offsetTop?: number;
}
const Sticky: FC<IProps> = ({ children, offsetTop = 65 }) => (
<StickyBox offsetTop={offsetTop} offsetBottom={10}>
{children}
</StickyBox>
);
export { Sticky };

View file

@ -0,0 +1,60 @@
import React, { createContext, FC, useContext, useMemo, useState, VFC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
interface TabProps {
items: string[];
}
const TabContext = createContext({
activeTab: 0,
setActiveTab: (activeTab: number) => {},
});
const HorizontalList: VFC<TabProps> = ({ items }) => {
const { activeTab, setActiveTab } = useContext(TabContext);
return (
<div className={styles.tabs}>
{items.map((it, index) => (
<div
className={classNames(styles.tab, { [styles.active]: activeTab === index })}
onClick={() => setActiveTab(index)}
key={it}
>
{it}
</div>
))}
</div>
);
};
const Content: FC<any> = ({ children }) => {
const { activeTab } = useContext(TabContext);
const notEmptyChildren = useMemo(() => {
if (!Array.isArray(children)) {
return [children];
}
return children.filter(it => it);
}, [children]);
if (Array.isArray(notEmptyChildren) && notEmptyChildren.length - 1 < activeTab) {
return notEmptyChildren[notEmptyChildren.length - 1];
}
return notEmptyChildren[activeTab];
};
const Tabs = function({ children }) {
const [activeTab, setActiveTab] = useState(0);
return <TabContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabContext.Provider>;
};
Tabs.Horizontal = HorizontalList;
Tabs.Content = Content;
export { Tabs };

View file

@ -0,0 +1,32 @@
@import 'src/styles/variables';
.wrap {
display: flex;
align-items: flex-start;
justify-content: flex-start;
padding: 0 $gap * 0.5;
}
.tab {
@include outer_shadow();
padding: $gap;
margin-right: $gap;
border-radius: $radius $radius 0 0;
font: $font_14_semibold;
text-transform: uppercase;
cursor: pointer;
background-color: $content_bg;
color: white;
text-decoration: none;
border: none;
&.active {
background: $content_bg_lighter;
}
}
.tabs {
display: flex;
flex-direction: row;
}

View file

@ -0,0 +1,9 @@
import React, { FC, HTMLAttributes } from 'react';
import styles from './styles.module.scss';
type IProps = HTMLAttributes<HTMLDivElement> & {};
const TagField: FC<IProps> = ({ children }) => <div className={styles.wrap}>{children}</div>;
export { TagField };

View file

@ -0,0 +1,18 @@
@import "src/styles/variables";
@import "src/styles/colors";
.wrap {
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
&> * {
margin: $gap * 0.5;
}
}
.edit {
opacity: 0.5;
float: right;
}

View file

@ -0,0 +1,33 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
interface ZoneProps {
title?: string;
className?: string;
color?: 'danger' | 'normal';
}
const Zone: FC<ZoneProps> = ({
title,
className,
children,
color = 'normal',
}) => (
<div
className={classNames(className, styles.pad, styles[color], {
[styles.with_title]: !!title,
})}
>
{!!title && (
<div className={styles.title}>
<span>{title}</span>
</div>
)}
{children}
</div>
);
export { Zone };

View file

@ -0,0 +1,39 @@
@import 'src/styles/variables';
$pad_usual: $content_bg_lightest;
.title {
position: relative;
span {
position: absolute;
top: -$gap;
left: $radius;
transform: translate(0, -100%);
background: $pad_usual;
border-radius: 4px;
font: $font_10_semibold;
line-height: 12px;
padding: 2px $gap * 0.5;
text-transform: uppercase;
.danger & {
background: $content_bg_danger;
}
}
}
.pad {
padding: $gap;
box-shadow: inset $pad_usual 0 0 0 2px;
border-radius: $radius;
position: relative;
&.danger {
box-shadow: inset $content_bg_danger 0 0 0 2px;
}
&.with_title {
padding-top: $gap * 2;
}
}