diff --git a/src/components/flow/Cell/index.tsx b/src/components/flow/Cell/index.tsx index bf4ab35a..ad7917f5 100644 --- a/src/components/flow/Cell/index.tsx +++ b/src/components/flow/Cell/index.tsx @@ -38,7 +38,6 @@ const Cell: FC = ({ }, [setIsLoaded]); const has_description = description && description.length > 32; - const text = (type === NODE_TYPES.TEXT && description) || (flow && flow.show_description && has_description && description) || diff --git a/src/components/flow/FlowCell/index.tsx b/src/components/flow/FlowCell/index.tsx index afc65ed1..8e908ee1 100644 --- a/src/components/flow/FlowCell/index.tsx +++ b/src/components/flow/FlowCell/index.tsx @@ -3,47 +3,84 @@ 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 { FlowDisplay, FlowDisplayVariant, INode } from '~/redux/types'; import { FlowCellText } from '~/components/flow/FlowCellText'; import classNames from 'classnames'; +import { FlowCellMenu } from '~/components/flow/FlowCellMenu'; +import { useFlowCellControls } from '~/utils/hooks/flow/useFlowCellControls'; interface Props { + id: INode['id']; to: string; title: string; image?: string; color?: string; text?: string; - display?: FlowDisplayVariant; + flow: FlowDisplay; + canEdit?: boolean; + onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; } -const FlowCell: FC = ({ color, to, image, display = 'single', text, title }) => { - const withText = ((!!display && display !== 'single') || !image) && !!text; +const FlowCell: FC = ({ + id, + color, + to, + image, + flow, + text, + title, + canEdit, + onChangeCellView, +}) => { + const withText = ((!!flow.display && flow.display !== 'single') || !image) && !!text; + const { + hasDescription, + setViewHorizontal, + setViewVertical, + setViewQuadro, + setViewSingle, + toggleViewDescription, + } = useFlowCellControls(id, text, flow, onChangeCellView); return ( - - {withText && ( - {title}}> - {text!} - - )} - - {image && ( - - )} - - - - {!withText && ( -
-

{title}

+
+ {canEdit && ( +
+
)} - + + {withText && ( + {title}}> + {text!} + + )} + + {image && ( + + )} + + + + {!withText && ( +
+

{title}

+
+ )} +
+
); }; diff --git a/src/components/flow/FlowCell/styles.module.scss b/src/components/flow/FlowCell/styles.module.scss index 5c47fc92..370d061d 100644 --- a/src/components/flow/FlowCell/styles.module.scss +++ b/src/components/flow/FlowCell/styles.module.scss @@ -6,19 +6,9 @@ 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 { @@ -95,3 +85,25 @@ font: $font_18_semibold; } } + +.menu { + position: absolute; + right: 0; + top: 0; + z-index: 6; +} + +.link { + display: flex; + width: 100%; + height: 100%; + flex-direction: row; + color: inherit; + text-decoration: inherit; + font: inherit; + line-height: inherit; + + &.vertical { + flex-direction: column-reverse; + } +} diff --git a/src/components/flow/FlowCellMenu/index.tsx b/src/components/flow/FlowCellMenu/index.tsx new file mode 100644 index 00000000..dd6a7874 --- /dev/null +++ b/src/components/flow/FlowCellMenu/index.tsx @@ -0,0 +1,67 @@ +import React, { FC } from 'react'; +import styles from './styles.module.scss'; +import { Icon } from '~/components/input/Icon'; +import { Manager, Popper, Reference } from 'react-popper'; +import { useFocusEvent } from '~/utils/hooks/useFocusEvent'; +import classNames from 'classnames'; +import { usePopperModifiers } from '~/utils/hooks/usePopperModifiers'; + +interface Props { + hasDescription: boolean; + toggleViewDescription: () => void; + setViewSingle: () => void; + setViewHorizontal: () => void; + setViewVertical: () => void; + setViewQuadro: () => void; +} + +const FlowCellMenu: FC = ({ + hasDescription, + toggleViewDescription, + setViewSingle, + setViewHorizontal, + setViewVertical, + setViewQuadro, +}) => { + const { onFocus, onBlur, focused } = useFocusEvent(); + const modifiers = usePopperModifiers(0, 10); + + return ( + + + + + {({ ref, style }) => ( +
+
+ {hasDescription && ( + <> + +
+ + )} + + + + +
+
+ )} + + + ); +}; + +export { FlowCellMenu }; diff --git a/src/components/flow/FlowCellMenu/styles.module.scss b/src/components/flow/FlowCellMenu/styles.module.scss new file mode 100644 index 00000000..ffd5da55 --- /dev/null +++ b/src/components/flow/FlowCellMenu/styles.module.scss @@ -0,0 +1,69 @@ +@import "~/styles/variables"; + +.button { + width: 48px; + height: 48px; + display: flex; + align-items: flex-start; + justify-content: flex-end; + fill: white; + padding: 5px; + box-sizing: border-box; + cursor: pointer; +} + +.dots { + @include blur($content_bg, 5px, 0.7); + + padding: 5px 0 0 0; + background: $content_bg; + border-radius: $radius; + width: 20px; + height: 32px; + position: relative; + + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + opacity: 0.5; + transition: opacity 0.25s; + + :hover > & { + opacity: 1; + } + } +} + +.dropdown { + @include dropdown_shadow; + @include blur($red, 15px, 0.3); + + border-radius: $radius; + padding: $gap; + visibility: hidden; + + &.active { + visibility: visible; + } +} + +.menu { + display: grid; + grid-auto-flow: row; + fill: white; + grid-row-gap: $gap; + + svg { + fill: white; + stroke: white; + cursor: pointer; + } +} + +.sep { + @include outer_shadow; + height: 1px; +} diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx index 5eaf4be9..6af28ff2 100644 --- a/src/components/flow/FlowGrid/index.tsx +++ b/src/components/flow/FlowGrid/index.tsx @@ -1,17 +1,18 @@ import React, { FC, Fragment } from 'react'; import { IFlowState } from '~/redux/flow/reducer'; -import { INode } from '~/redux/types'; +import { FlowDisplay, INode } from '~/redux/types'; import { IUser } from '~/redux/auth/types'; import { PRESETS, 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'; +import { canEditNode } from '~/utils/node'; type IProps = Partial & { user: Partial; - onChangeCellView: (id: INode['id'], flow: INode['flow']) => void; + onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; }; export const FlowGrid: FC = ({ user, nodes, onChangeCellView }) => { @@ -24,12 +25,15 @@ export const FlowGrid: FC = ({ user, nodes, onChangeCellView }) => { {nodes.map(node => (
))} diff --git a/src/layouts/FlowLayout/index.tsx b/src/layouts/FlowLayout/index.tsx index ea2736e9..c88aebbb 100644 --- a/src/layouts/FlowLayout/index.tsx +++ b/src/layouts/FlowLayout/index.tsx @@ -15,7 +15,7 @@ import { FlowStamp } from '~/components/flow/FlowStamp'; import { Container } from '~/containers/main/Container'; import { SidebarRouter } from '~/containers/main/SidebarRouter'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; -import { INode } from '~/redux/types'; +import { FlowDisplay, INode } from '~/redux/types'; import { selectLabUpdatesNodes } from '~/redux/lab/selectors'; import { usePersistedState } from '~/utils/hooks/usePersistedState'; import classNames from 'classnames'; @@ -44,18 +44,16 @@ const FlowLayout: FC = () => { [dispatch] ); - const onChangeCellView = useCallback( - (id: INode['id'], flow: INode['flow']) => { - dispatch(flowSetCellView(id, flow)); - }, - [dispatch] - ); - const cumulativeUpdates = useMemo(() => [...updated, ...labUpdates].slice(0, 10), [ updated, labUpdates, ]); + const onChangeCellView = useCallback( + (id: INode['id'], val: FlowDisplay) => dispatch(flowSetCellView(id, val)), + [] + ); + return (
diff --git a/src/redux/types.ts b/src/redux/types.ts index 0d4306fa..152e8eea 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -112,6 +112,11 @@ export interface IBlockEmbed { export type IBlock = IBlockText | IBlockEmbed; export type FlowDisplayVariant = 'single' | 'vertical' | 'horizontal' | 'quadro'; +export interface FlowDisplay { + display: FlowDisplayVariant; + show_description: boolean; + dominant_color?: string; +} export interface INode { id?: number; @@ -132,11 +137,7 @@ export interface INode { is_public?: boolean; like_count?: number; - flow: { - display: FlowDisplayVariant; - show_description: boolean; - dominant_color?: string; - }; + flow: FlowDisplay; tags: ITag[]; diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx index ba0e79d6..530ad045 100644 --- a/src/sprites/Sprites.tsx +++ b/src/sprites/Sprites.tsx @@ -342,6 +342,14 @@ const Sprites: FC = () => ( d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z" /> + + + + + ); diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 5b895b79..5dda5ddd 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -94,6 +94,14 @@ $sidebar_border: transparentize(white, 0.95); transparentize(black, 0.6) 0 1px 5px; } +// same as outer shadow, but higher +@mixin dropdown_shadow { + box-shadow: + inset transparentize(white, 0.95) 1px 1px, + transparentize(black, 0.8) 1px 1px, + transparentize(black, 0.6) 5px 5px 10px; +} + @mixin row_shadow() { &:not(:last-child) { box-shadow: transparentize(white, 0.95) 0 1px, diff --git a/src/utils/hooks/flow/useFlowCellControls.ts b/src/utils/hooks/flow/useFlowCellControls.ts new file mode 100644 index 00000000..68c78e2c --- /dev/null +++ b/src/utils/hooks/flow/useFlowCellControls.ts @@ -0,0 +1,46 @@ +import { useCallback } from 'react'; +import { FlowDisplay, INode } from '~/redux/types'; + +export const useFlowCellControls = ( + id: INode['id'], + description: string | undefined, + flow: FlowDisplay, + onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void +) => { + const onChange = useCallback( + (value: Partial) => onChangeCellView(id, { ...flow, ...value }), + [] + ); + + const hasDescription = !!description && description.length > 32; + + const toggleViewDescription = useCallback(() => { + const show_description = !(flow && flow.show_description); + onChange({ show_description }); + }, [id, flow, onChange]); + + const setViewSingle = useCallback(() => { + onChange({ display: 'single' }); + }, [id, flow, onChange]); + + const setViewHorizontal = useCallback(() => { + onChange({ display: 'horizontal' }); + }, [id, flow, onChange]); + + const setViewVertical = useCallback(() => { + onChange({ display: 'vertical' }); + }, [id, flow]); + + const setViewQuadro = useCallback(() => { + onChange({ display: 'quadro' }); + }, [id, flow, onChange]); + + return { + hasDescription, + setViewHorizontal, + setViewVertical, + setViewQuadro, + setViewSingle, + toggleViewDescription, + }; +}; diff --git a/src/utils/hooks/useFocusEvent.ts b/src/utils/hooks/useFocusEvent.ts new file mode 100644 index 00000000..6ae1c588 --- /dev/null +++ b/src/utils/hooks/useFocusEvent.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react'; + +export const useFocusEvent = (initialState = false) => { + const [focused, setFocused] = useState(initialState); + + const onFocus = useCallback( + event => { + event.preventDefault(); + event.stopPropagation(); + + setFocused(true); + }, + [setFocused] + ); + const onBlur = useCallback(() => setTimeout(() => setFocused(false), 300), [setFocused]); + + return { focused, onBlur, onFocus }; +}; diff --git a/src/utils/hooks/usePopperModifiers.ts b/src/utils/hooks/usePopperModifiers.ts new file mode 100644 index 00000000..250931bf --- /dev/null +++ b/src/utils/hooks/usePopperModifiers.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import { Modifier } from 'react-popper'; + +const sameWidth = { + name: 'sameWidth', + enabled: true, + phase: 'beforeWrite', + requires: ['computeStyles'], + fn: ({ state }: { state: any }) => { + // eslint-disable-next-line no-param-reassign + state.styles.popper.width = `${state.rects.reference.width}px`; + }, + effect: ({ state }: { state: any }) => { + // eslint-disable-next-line no-param-reassign + state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`; + }, +}; + +export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean): Modifier[] => + useMemo( + () => + [ + { + name: 'offset', + options: { + offset: [offsetX, offsetY], + }, + }, + { + name: 'preventOverflow', + options: { + padding: 10, + }, + }, + ...(justify ? [sameWidth] : []), + ] as Modifier[], + [offsetX, offsetY, justify] + );