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:
parent
03ddb1862c
commit
5e9c111e0f
149 changed files with 416 additions and 317 deletions
76
src/components/common/BetterScrollDialog/index.tsx
Normal file
76
src/components/common/BetterScrollDialog/index.tsx
Normal 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 };
|
150
src/components/common/BetterScrollDialog/styles.module.scss
Normal file
150
src/components/common/BetterScrollDialog/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
11
src/components/common/BlurWrapper/index.tsx
Normal file
11
src/components/common/BlurWrapper/index.tsx
Normal 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>
|
||||
);
|
9
src/components/common/BlurWrapper/styles.module.scss
Normal file
9
src/components/common/BlurWrapper/styles.module.scss
Normal 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;
|
||||
}
|
34
src/components/common/Card/index.tsx
Normal file
34
src/components/common/Card/index.tsx
Normal 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 };
|
24
src/components/common/Card/styles.module.scss
Normal file
24
src/components/common/Card/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
22
src/components/common/CellGrid/index.tsx
Normal file
22
src/components/common/CellGrid/index.tsx
Normal 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 };
|
8
src/components/common/CellGrid/styles.module.scss
Normal file
8
src/components/common/CellGrid/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-column-gap: $gap;
|
||||
grid-row-gap: $gap;
|
||||
}
|
55
src/components/common/Columns/index.tsx
Normal file
55
src/components/common/Columns/index.tsx
Normal 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 };
|
28
src/components/common/Columns/styles.module.scss
Normal file
28
src/components/common/Columns/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
47
src/components/common/CoverBackdrop/index.tsx
Normal file
47
src/components/common/CoverBackdrop/index.tsx
Normal 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 };
|
44
src/components/common/CoverBackdrop/styles.module.scss
Normal file
44
src/components/common/CoverBackdrop/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
11
src/components/common/DialogTitle/index.tsx
Normal file
11
src/components/common/DialogTitle/index.tsx
Normal 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 };
|
6
src/components/common/DialogTitle/styles.module.scss
Normal file
6
src/components/common/DialogTitle/styles.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.title {
|
||||
margin: $gap 0 $gap * 4 !important;
|
||||
text-transform: uppercase;
|
||||
}
|
11
src/components/common/Filler/index.tsx
Normal file
11
src/components/common/Filler/index.tsx
Normal 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} />
|
||||
);
|
5
src/components/common/Filler/styles.module.scss
Normal file
5
src/components/common/Filler/styles.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.filler {
|
||||
flex: 1;
|
||||
}
|
56
src/components/common/Grid/index.tsx
Normal file
56
src/components/common/Grid/index.tsx
Normal 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 };
|
24
src/components/common/Grid/styles.module.scss
Normal file
24
src/components/common/Grid/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
44
src/components/common/Group/index.tsx
Normal file
44
src/components/common/Group/index.tsx
Normal 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 };
|
49
src/components/common/Group/styles.module.scss
Normal file
49
src/components/common/Group/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
44
src/components/common/InfiniteScroll/index.tsx
Normal file
44
src/components/common/InfiniteScroll/index.tsx
Normal 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 };
|
4
src/components/common/InfiniteScroll/styles.module.scss
Normal file
4
src/components/common/InfiniteScroll/styles.module.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.more {
|
||||
}
|
21
src/components/common/Markdown/index.tsx
Normal file
21
src/components/common/Markdown/index.tsx
Normal 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 };
|
21
src/components/common/ModalWrapper/index.tsx
Normal file
21
src/components/common/ModalWrapper/index.tsx
Normal 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 };
|
58
src/components/common/ModalWrapper/styles.module.scss
Normal file
58
src/components/common/ModalWrapper/styles.module.scss
Normal 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;
|
||||
}
|
31
src/components/common/Padder/index.tsx
Normal file
31
src/components/common/Padder/index.tsx
Normal 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 };
|
15
src/components/common/Padder/styles.module.scss
Normal file
15
src/components/common/Padder/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
18
src/components/common/Panel/index.tsx
Normal file
18
src/components/common/Panel/index.tsx
Normal 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 };
|
16
src/components/common/Panel/styles.module.scss
Normal file
16
src/components/common/Panel/styles.module.scss
Normal 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; }
|
||||
}
|
15
src/components/common/Sticky/index.tsx
Normal file
15
src/components/common/Sticky/index.tsx
Normal 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 };
|
60
src/components/common/Tabs/index.tsx
Normal file
60
src/components/common/Tabs/index.tsx
Normal 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 };
|
32
src/components/common/Tabs/styles.module.scss
Normal file
32
src/components/common/Tabs/styles.module.scss
Normal 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;
|
||||
}
|
9
src/components/common/TagField/index.tsx
Normal file
9
src/components/common/TagField/index.tsx
Normal 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 };
|
18
src/components/common/TagField/styles.module.scss
Normal file
18
src/components/common/TagField/styles.module.scss
Normal 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;
|
||||
}
|
33
src/components/common/Zone/index.tsx
Normal file
33
src/components/common/Zone/index.tsx
Normal 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 };
|
39
src/components/common/Zone/styles.module.scss
Normal file
39
src/components/common/Zone/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue