mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
refactored flow cells, added colors for lab (#78)
* made better flow cells * made cubical desaturation * made colorfull lab nodes * colorful lab nodes for all text ones * all lab nodes are colorful * disabled lazy loading on heroes * fixed color calculation hook * fixed lab color gradients calculation * fixed cell text on flow
This commit is contained in:
parent
7d6f35b0af
commit
94c656fe0f
29 changed files with 345 additions and 63 deletions
|
@ -1,3 +1,3 @@
|
||||||
#REACT_APP_API_HOST=http://localhost:3334/
|
#REACT_APP_API_HOST=http://localhost:3334/
|
||||||
REACT_APP_API_HOST=https://pig.staging.vault48.org/
|
REACT_APP_API_HOST=https://pig.vault48.org/
|
||||||
REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
REACT_APP_REMOTE_CURRENT=https://pig.vault48.org/static/
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-dropzone": "^11.4.2",
|
"react-dropzone": "^11.4.2",
|
||||||
|
"react-lazyload": "^3.2.0",
|
||||||
"react-masonry-css": "^1.0.16",
|
"react-masonry-css": "^1.0.16",
|
||||||
"react-popper": "^2.2.3",
|
"react-popper": "^2.2.3",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
||||||
import { convertHexToRGBA } from '~/utils/color';
|
|
||||||
import { DivProps } from '~/utils/types';
|
import { DivProps } from '~/utils/types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { transparentize } from 'color2k';
|
||||||
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
|
|
||||||
interface Props extends DivProps {
|
interface Props extends DivProps {
|
||||||
color?: string;
|
color?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CellShade: FC<Props> = ({ color, ...rest }) => {
|
const CellShade: FC<Props> = ({ color, size = 50, ...rest }) => {
|
||||||
const background = useMemo(() => {
|
const background = useMemo(() => {
|
||||||
if (!color || color === DEFAULT_DOMINANT_COLOR) {
|
const normalized = normalizeBrightColor(color);
|
||||||
|
|
||||||
|
if (!color || color === DEFAULT_DOMINANT_COLOR || !normalized) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `linear-gradient(7deg, ${color} 50px, ${convertHexToRGBA(color, 0.3)} 250px)`;
|
return `linear-gradient(7deg, ${normalized} ${size}px, ${transparentize(normalized, 1)} ${size *
|
||||||
|
5}px)`;
|
||||||
}, [color]);
|
}, [color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
@import "~/styles/variables";
|
@import "~/styles/variables";
|
||||||
|
|
||||||
.shade {
|
.shade {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
background: linear-gradient(7deg, transparentize($content_bg, 0.05) 30px, transparentize($content_bg, 1) 250px);
|
background: linear-gradient(7deg, transparentize($content_bg, 0.05) 30px, transparentize($content_bg, 1) 250px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|
||||||
@include tablet {
|
&.black::after {
|
||||||
opacity: 0.7;
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
50
src/components/flow/FlowCell/index.tsx
Normal file
50
src/components/flow/FlowCell/index.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { CellShade } from '~/components/flow/CellShade';
|
||||||
|
import { FlowCellImage } from '~/components/flow/FlowCellImage';
|
||||||
|
import { FlowDisplayVariant } from '~/redux/types';
|
||||||
|
import { FlowCellText } from '~/components/flow/FlowCellText';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
to: string;
|
||||||
|
title: string;
|
||||||
|
image?: string;
|
||||||
|
color?: string;
|
||||||
|
text?: string;
|
||||||
|
display?: FlowDisplayVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowCell: FC<Props> = ({ color, to, image, display = 'single', text, title }) => {
|
||||||
|
const withText = ((!!display && display !== 'single') || !image) && !!text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink className={classNames(styles.cell, styles[display || 'single'])} to={to}>
|
||||||
|
{withText && (
|
||||||
|
<FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
|
||||||
|
{text!}
|
||||||
|
</FlowCellText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{image && (
|
||||||
|
<FlowCellImage
|
||||||
|
src={image}
|
||||||
|
height={400}
|
||||||
|
className={styles.thumb}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CellShade color={color} className={styles.shade} size={withText ? 15 : 50} />
|
||||||
|
|
||||||
|
{!withText && (
|
||||||
|
<div className={styles.title_wrapper}>
|
||||||
|
<h4 className={styles.title}>{title}</h4>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { FlowCell };
|
97
src/components/flow/FlowCell/styles.module.scss
Normal file
97
src/components/flow/FlowCell/styles.module.scss
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
@import "~/styles/variables";
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
@include inner_shadow;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $radius;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: $content_bg;
|
||||||
|
flex-direction: row;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
@include outer_shadow;
|
||||||
|
|
||||||
|
border-radius: $radius;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shade {
|
||||||
|
@include outer_shadow;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $radius;
|
||||||
|
max-height: calc(100% - 10px);
|
||||||
|
max-width: calc(100% - 10px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: $font_16_regular;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
font: $font_14_regular;
|
||||||
|
left: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& :global(.grey) {
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quadro &,
|
||||||
|
.horizontal & {
|
||||||
|
max-width: calc(50% - 15px);
|
||||||
|
@include blur(transparentize($content_bg, 0), 10px, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.quadro &,
|
||||||
|
.vertical & {
|
||||||
|
max-height: calc(50% - 15px);
|
||||||
|
@include blur(transparentize($content_bg, 0), 10px, 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title_wrapper {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
padding: $gap;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_cell_title;
|
||||||
|
text-transform: uppercase;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
font: $font_18_semibold;
|
||||||
|
}
|
||||||
|
}
|
18
src/components/flow/FlowCellImage/index.tsx
Normal file
18
src/components/flow/FlowCellImage/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import LazyLoad from 'react-lazyload';
|
||||||
|
import { IMGProps } from '~/utils/types';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface Props extends IMGProps {
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowCellImage: FC<Props> = ({ className, children, ...rest }) => (
|
||||||
|
<LazyLoad once className={classNames(styles.wrapper, className)}>
|
||||||
|
<img {...rest} src={rest.src} alt="" />
|
||||||
|
{children}
|
||||||
|
</LazyLoad>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { FlowCellImage };
|
15
src/components/flow/FlowCellImage/styles.module.scss
Normal file
15
src/components/flow/FlowCellImage/styles.module.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
23
src/components/flow/FlowCellText/index.tsx
Normal file
23
src/components/flow/FlowCellText/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { FC, ReactElement } from 'react';
|
||||||
|
import { Markdown } from '~/components/containers/Markdown';
|
||||||
|
import { DivProps } from '~/utils/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { formatText } from '~/utils/dom';
|
||||||
|
|
||||||
|
interface Props extends DivProps {
|
||||||
|
children: string;
|
||||||
|
heading: string | ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => (
|
||||||
|
<div {...rest} className={classNames(styles.text, rest.className)}>
|
||||||
|
{heading && <div className={styles.heading}>{heading}</div>}
|
||||||
|
<Markdown
|
||||||
|
className={styles.description}
|
||||||
|
dangerouslySetInnerHTML={{ __html: formatText(children) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { FlowCellText };
|
11
src/components/flow/FlowCellText/styles.module.scss
Normal file
11
src/components/flow/FlowCellText/styles.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@import "~/styles/variables";
|
||||||
|
|
||||||
|
.text {
|
||||||
|
padding: $gap;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { FC, Fragment, useCallback } from 'react';
|
import React, { FC, Fragment } from 'react';
|
||||||
import { Cell } from '~/components/flow/Cell';
|
|
||||||
|
|
||||||
import { IFlowState } from '~/redux/flow/reducer';
|
import { IFlowState } from '~/redux/flow/reducer';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { canEditNode } from '~/utils/node';
|
|
||||||
import { IUser } from '~/redux/auth/types';
|
import { IUser } from '~/redux/auth/types';
|
||||||
import { useHistory } from 'react-router';
|
import { PRESETS, URLS } from '~/constants/urls';
|
||||||
import { URLS } from '~/constants/urls';
|
import { FlowCell } from '~/components/flow/FlowCell';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { getURLFromString } from '~/utils/dom';
|
||||||
|
|
||||||
type IProps = Partial<IFlowState> & {
|
type IProps = Partial<IFlowState> & {
|
||||||
user: Partial<IUser>;
|
user: Partial<IUser>;
|
||||||
|
@ -14,9 +15,6 @@ type IProps = Partial<IFlowState> & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => {
|
export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => {
|
||||||
const history = useHistory();
|
|
||||||
const onSelect = useCallback((id: INode['id']) => history.push(URLS.NODE_URL(id)), [history]);
|
|
||||||
|
|
||||||
if (!nodes) {
|
if (!nodes) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -24,13 +22,16 @@ export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{nodes.map(node => (
|
{nodes.map(node => (
|
||||||
<Cell
|
<div className={classNames(styles.cell, styles[node.flow.display])} key={node.id}>
|
||||||
key={node.id}
|
<FlowCell
|
||||||
node={node}
|
color={node.flow.dominant_color}
|
||||||
onSelect={onSelect}
|
to={URLS.NODE_URL(node.id)}
|
||||||
can_edit={canEditNode(node, user)}
|
image={getURLFromString(node.thumbnail, PRESETS.cover)}
|
||||||
onChangeCellView={onChangeCellView}
|
display={node.flow.display}
|
||||||
/>
|
text={node.flow.show_description ? node.description : ''}
|
||||||
|
title={node.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
@import "~/styles/variables";
|
||||||
|
|
||||||
|
@mixin mobile {
|
||||||
|
@media (max-width: $cell * 2) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
&.horizontal,
|
||||||
|
&.quadro {
|
||||||
|
grid-column-end: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical,
|
||||||
|
&.quadro {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
}
|
||||||
|
}
|
|
@ -100,7 +100,8 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||||
speed={3000}
|
speed={3000}
|
||||||
className={styles.swiper}
|
className={styles.swiper}
|
||||||
lazy={{
|
lazy={{
|
||||||
loadPrevNextAmount: 3,
|
loadPrevNextAmount: 5,
|
||||||
|
checkInView: false,
|
||||||
}}
|
}}
|
||||||
loop
|
loop
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
|
@ -122,7 +123,7 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
|
||||||
.map(node => (
|
.map(node => (
|
||||||
<SwiperSlide key={node.id}>
|
<SwiperSlide key={node.id}>
|
||||||
<img
|
<img
|
||||||
data-src={getURLFromString(node.thumbnail!, preset)}
|
src={getURLFromString(node.thumbnail!, preset)}
|
||||||
alt=""
|
alt=""
|
||||||
className={classNames(styles.preview, 'swiper-lazy')}
|
className={classNames(styles.preview, 'swiper-lazy')}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,7 +23,6 @@ interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const breakpoints: SwiperOptions['breakpoints'] = {
|
const breakpoints: SwiperOptions['breakpoints'] = {
|
||||||
599: {
|
599: {
|
||||||
spaceBetween: 20,
|
|
||||||
navigation: true,
|
navigation: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -64,7 +63,6 @@ const LabImage: FC<IProps> = ({ node, isLoading }) => {
|
||||||
initialSlide={0}
|
initialSlide={0}
|
||||||
slidesPerView={images.length > 1 ? 1.1 : 1}
|
slidesPerView={images.length > 1 ? 1.1 : 1}
|
||||||
onSwiper={setControlledSwiper}
|
onSwiper={setControlledSwiper}
|
||||||
spaceBetween={10}
|
|
||||||
grabCursor
|
grabCursor
|
||||||
autoHeight
|
autoHeight
|
||||||
breakpoints={breakpoints}
|
breakpoints={breakpoints}
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font: $font_32_bold;
|
font: $font_32_bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: $radius;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -51,7 +50,6 @@
|
||||||
.image {
|
.image {
|
||||||
max-height: calc(100vh - 70px - 70px);
|
max-height: calc(100vh - 70px - 70px);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: $radius;
|
|
||||||
transition: box-shadow 1s;
|
transition: box-shadow 1s;
|
||||||
box-shadow: transparentize(black, 0.7) 0 3px 5px;
|
box-shadow: transparentize(black, 0.7) 0 3px 5px;
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import styles from './styles.module.scss';
|
||||||
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
|
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
|
||||||
import { isAfter, parseISO } from 'date-fns';
|
import { isAfter, parseISO } from 'date-fns';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useColorGradientFromString } from '~/utils/hooks/useColorGradientFromString';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
@ -22,8 +23,10 @@ const LabNode: FC<IProps> = ({ node, isLoading, lastSeen, commentCount }) => {
|
||||||
[node.commented_at, lastSeen]
|
[node.commented_at, lastSeen]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const background = useColorGradientFromString(node.title, 3, 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.wrap, { [styles.heroic]: node.is_heroic })}>
|
<div className={classNames(styles.wrap)} style={{ background }}>
|
||||||
{lab}
|
{lab}
|
||||||
<LabBottomPanel
|
<LabBottomPanel
|
||||||
node={node}
|
node={node}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import SwiperClass from 'swiper/types/swiper-class';
|
||||||
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { ImagePreloader } from '~/components/media/ImagePreloader';
|
import { ImagePreloader } from '~/components/media/ImagePreloader';
|
||||||
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
|
|
||||||
SwiperCore.use([Navigation, Pagination, Keyboard]);
|
SwiperCore.use([Navigation, Pagination, Keyboard]);
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
onLoad={updateSwiper}
|
onLoad={updateSwiper}
|
||||||
onClick={() => onOpenPhotoSwipe(i)}
|
onClick={() => onOpenPhotoSwipe(i)}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
color={file?.metadata?.dominant_color}
|
color={normalizeBrightColor(file?.metadata?.dominant_color)}
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import { PRESETS, URLS } from '~/constants/urls';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router';
|
import { RouteComponentProps, withRouter } from 'react-router';
|
||||||
import { getURL, stringToColour } from '~/utils/dom';
|
import { getURL, stringToColour } from '~/utils/dom';
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
|
import { adjustHue } from 'color2k';
|
||||||
|
import { useColorGradientFromString } from '~/utils/hooks/useColorGradientFromString';
|
||||||
|
|
||||||
type IProps = RouteComponentProps & {
|
type IProps = RouteComponentProps & {
|
||||||
item: Partial<INode>;
|
item: Partial<INode>;
|
||||||
|
@ -37,10 +40,8 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
|
||||||
() => (item.thumbnail ? getURL({ url: item.thumbnail }, PRESETS.avatar) : ''),
|
() => (item.thumbnail ? getURL({ url: item.thumbnail }, PRESETS.avatar) : ''),
|
||||||
[item]
|
[item]
|
||||||
);
|
);
|
||||||
const backgroundColor = useMemo(
|
|
||||||
() => (!thumb && item.title && stringToColour(item.title)) || '',
|
const background = useColorGradientFromString(!thumb ? item.title : '');
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
@ -76,13 +77,13 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!item.thumbnail && size === 'small' && (
|
{!item.thumbnail && size === 'small' && (
|
||||||
<div className={styles.letters} style={{ backgroundColor }}>
|
<div className={styles.letters} style={{ background }}>
|
||||||
{getTitleLetters(item.title)}
|
{getTitleLetters(item.title)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!item.thumbnail && size !== 'small' && (
|
{!item.thumbnail && size !== 'small' && (
|
||||||
<div className={styles.title} style={{ backgroundColor }}>
|
<div className={styles.title} style={{ background }}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -43,11 +43,6 @@ div.thumb {
|
||||||
font: $font_24_semibold;
|
font: $font_24_semibold;
|
||||||
color: transparentize(white, 0.5);
|
color: transparentize(white, 0.5);
|
||||||
border-radius: $cell_radius;
|
border-radius: $cell_radius;
|
||||||
background-image: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
transparentize($content_bg, 0.4),
|
|
||||||
transparentize($content_bg, 0.4)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -111,6 +111,7 @@ export interface IBlockEmbed {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IBlock = IBlockText | IBlockEmbed;
|
export type IBlock = IBlockText | IBlockEmbed;
|
||||||
|
export type FlowDisplayVariant = 'single' | 'vertical' | 'horizontal' | 'quadro';
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
@ -132,7 +133,7 @@ export interface INode {
|
||||||
like_count?: number;
|
like_count?: number;
|
||||||
|
|
||||||
flow: {
|
flow: {
|
||||||
display: 'single' | 'vertical' | 'horizontal' | 'quadro';
|
display: FlowDisplayVariant;
|
||||||
show_description: boolean;
|
show_description: boolean;
|
||||||
dominant_color?: string;
|
dominant_color?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,10 @@ body {
|
||||||
background-size: 600px 600px;
|
background-size: 600px 600px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|
|
@ -112,7 +112,7 @@ $margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.grey) {
|
:global(.grey) {
|
||||||
color: #555555;
|
color: #666666;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,13 +194,13 @@ $sidebar_border: transparentize(white, 0.95);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin blur($color: $content_bg, $radius: 15px) {
|
@mixin blur($color: $content_bg, $radius: 15px, $opacity: 0.5) {
|
||||||
background: transparentize($color, 0.1);
|
background: transparentize($color, $opacity / 2);
|
||||||
|
|
||||||
@include can_backdrop {
|
@include can_backdrop {
|
||||||
backdrop-filter: blur($radius);
|
backdrop-filter: blur($radius);
|
||||||
-webkit-backdrop-filter: blur($radius);
|
-webkit-backdrop-filter: blur($radius);
|
||||||
background: transparentize($color, 0.5);
|
background: transparentize($color, $opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
export const convertHexToRGBA = (hexCode, opacity) => {
|
import { darken, desaturate, parseToHsla } from 'color2k';
|
||||||
let hex = hexCode.replace('#', '');
|
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
||||||
|
|
||||||
if (hex.length === 3) {
|
export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnessExp = 3) => {
|
||||||
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
|
if (!color) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = parseInt(hex.substring(0, 2), 16);
|
const hsla = parseToHsla(color || DEFAULT_DOMINANT_COLOR);
|
||||||
const g = parseInt(hex.substring(2, 4), 16);
|
const saturation = hsla[1];
|
||||||
const b = parseInt(hex.substring(4, 6), 16);
|
const lightness = hsla[2];
|
||||||
|
|
||||||
return `rgba(${r},${g},${b},${opacity})`;
|
const desaturated = desaturate(color, saturation ** saturationExp);
|
||||||
|
return darken(desaturated, lightness ** lightnessExp);
|
||||||
};
|
};
|
||||||
|
|
|
@ -76,17 +76,17 @@ export const describeArc = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getURLFromString = (
|
export const getURLFromString = (
|
||||||
url: string,
|
url?: string,
|
||||||
size?: typeof PRESETS[keyof typeof PRESETS]
|
size?: typeof PRESETS[keyof typeof PRESETS]
|
||||||
): string => {
|
): string => {
|
||||||
if (size) {
|
if (size) {
|
||||||
return url.replace(
|
return (url || '').replace(
|
||||||
'REMOTE_CURRENT://',
|
'REMOTE_CURRENT://',
|
||||||
`${process.env.REACT_APP_REMOTE_CURRENT}cache/${size}/`
|
`${process.env.REACT_APP_REMOTE_CURRENT}cache/${size}/`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT);
|
return (url || '').replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getURL = (
|
export const getURL = (
|
||||||
|
|
10
src/utils/hooks/useColorFromString.ts
Normal file
10
src/utils/hooks/useColorFromString.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
|
import { stringToColour } from '~/utils/dom';
|
||||||
|
|
||||||
|
export const useColorFromString = (val?: string, saturation = 3, lightness = 3) => {
|
||||||
|
return useMemo(
|
||||||
|
() => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
17
src/utils/hooks/useColorGradientFromString.ts
Normal file
17
src/utils/hooks/useColorGradientFromString.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { adjustHue } from 'color2k';
|
||||||
|
import { normalizeBrightColor } from '~/utils/color';
|
||||||
|
import { stringToColour } from '~/utils/dom';
|
||||||
|
|
||||||
|
export const useColorGradientFromString = (val?: string, saturation = 3, lightness = 3) =>
|
||||||
|
useMemo(() => {
|
||||||
|
if (!val) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = normalizeBrightColor(stringToColour(val), saturation, lightness);
|
||||||
|
const second = normalizeBrightColor(adjustHue(color, 45), saturation, lightness);
|
||||||
|
const third = normalizeBrightColor(adjustHue(color, 90), saturation, lightness);
|
||||||
|
|
||||||
|
return `linear-gradient(155deg, ${color}, ${second}, ${third})`;
|
||||||
|
}, [val]);
|
|
@ -6,3 +6,8 @@ export type DivProps = React.DetailedHTMLProps<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type SVGProps = React.SVGProps<SVGSVGElement>;
|
export type SVGProps = React.SVGProps<SVGSVGElement>;
|
||||||
|
|
||||||
|
export type IMGProps = React.DetailedHTMLProps<
|
||||||
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
|
HTMLImageElement
|
||||||
|
>;
|
||||||
|
|
|
@ -9417,6 +9417,11 @@ react-is@^17.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||||
|
|
||||||
|
react-lazyload@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-3.2.0.tgz#497bd06a6dbd7015e3376e1137a67dc47d2dd021"
|
||||||
|
integrity sha512-zJlrG8QyVZz4+xkYZH5v1w3YaP5wEFaYSUWC4CT9UXfK75IfRAIEdnyIUF+dXr3kX2MOtL1lUaZmaQZqrETwgw==
|
||||||
|
|
||||||
react-masonry-css@^1.0.16:
|
react-masonry-css@^1.0.16:
|
||||||
version "1.0.16"
|
version "1.0.16"
|
||||||
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
|
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue