1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46: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:
muerwre 2021-10-08 11:33:53 +07:00 committed by GitHub
parent 7d6f35b0af
commit 94c656fe0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 345 additions and 63 deletions

View file

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

View file

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

View file

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

View file

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

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

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

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

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

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

View file

@ -0,0 +1,11 @@
@import "~/styles/variables";
.text {
padding: $gap;
line-height: 1.3em;
}
.heading {
margin-bottom: 0.4em;
}

View file

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

View file

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

View file

@ -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')}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -112,7 +112,7 @@ $margin: 1em;
} }
:global(.grey) { :global(.grey) {
color: #555555; color: #666666;
white-space: pre-line; white-space: pre-line;
} }
} }

View file

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

View file

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

View file

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

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

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

View file

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

View file

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