feature: add more to project pane

This commit is contained in:
Jordan Knott
2020-05-26 19:53:31 -05:00
parent 7e78ee36b4
commit fba4de631f
64 changed files with 1845 additions and 582 deletions

View File

@ -4,25 +4,51 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = {
label: Label;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
<FieldName
id="labelName"
type="text"
name="name"
onChange={e => {
setCurrentLabel(e.currentTarget.value);
}}
value={currentLabel}
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
<LabelBox color={labelColor}>
<Checkmark color="#fff" size={12} />
<LabelBox
color={labelColor}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{labelColor === currentColor && <Checkmark color="#fff" size={12} />}
</LabelBox>
))}
</div>
<div>
<SaveButton type="submit" value="Save" />
<SaveButton
onClick={e => {
e.preventDefault();
console.log(currentColor);
if (currentColor) {
onLabelEdit(label ? label.labelId : null, currentLabel, currentColor);
}
}}
type="submit"
value="Save"
/>
<DeleteButton type="submit" value="Delete" />
</div>
</EditLabelForm>

View File

@ -1,46 +1,78 @@
import React, { useState } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
import {
LabelSearch,
ActiveIcon,
Labels,
Label,
CardLabel,
Section,
SectionTitle,
LabelIcon,
CreateLabelButton,
} from './Styles';
type Props = {
labels?: Label[];
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
onLabelEdit: (labelId: string) => void;
onLabelCreate: () => void;
};
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit }) => {
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onLabelCreate }) => {
const [currentLabel, setCurrentLabel] = useState('');
const [currentSearch, setCurrentSearch] = useState('');
return (
<>
<LabelSearch type="text" />
<LabelSearch
type="text"
placeholder="search labels..."
onChange={e => {
setCurrentSearch(e.currentTarget.value);
}}
value={currentSearch}
/>
<Section>
<SectionTitle>Labels</SectionTitle>
<Labels>
{labels &&
labels.map(label => (
<Label>
<LabelIcon>
<Pencil />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
labels
.filter(label => currentSearch === '' || label.name.toLowerCase().startsWith(currentSearch.toLowerCase()))
.map(label => (
<Label key={label.labelId}>
<LabelIcon
onClick={() => {
onLabelEdit(label.labelId);
}}
>
<Pencil color="#c2c6dc" />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
</Labels>
<CreateLabelButton
onClick={() => {
onLabelCreate();
}}
>
Create a new label
</CreateLabelButton>
</Section>
</>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, createRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
@ -7,10 +7,12 @@ import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import DueDateManager from 'shared/components/DueDateManager';
import MiniProfile from 'shared/components/MiniProfile';
import styled from 'styled-components';
import PopupMenu from '.';
import PopupMenu, { PopupProvider, usePopup, Popup } from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import produce from 'immer';
export default {
component: PopupMenu,
@ -37,19 +39,93 @@ const labelData = [
},
];
const OpenLabelBtn = styled.span``;
type TabProps = {
tab: number;
};
const LabelManagerEditor = () => {
const [labels, setLabels] = useState(labelData);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={action('on close')}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
const OpenLabelsButton = () => {
const $buttonRef = createRef<HTMLButtonElement>();
const [currentLabel, setCurrentLabel] = useState('');
const [labels, setLabels] = useState(labelData);
const { showPopup, setTab } = usePopup();
console.log(labels);
return (
<OpenLabelBtn
ref={$buttonRef}
onClick={() => {
showPopup($buttonRef, <LabelManagerEditor />);
}}
>
Open
</OpenLabelBtn>
);
};
export const LabelsPopup = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu title="Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelManager labels={labelData} onLabelToggle={action('label toggle')} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
<PopupProvider>
<OpenLabelsButton />
</PopupProvider>
);
};
@ -58,7 +134,13 @@ export const LabelsLabelEditor = () => {
return (
<>
{isPopupOpen && (
<PopupMenu title="Change Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<PopupMenu
onPrevious={action('on previous')}
title="Change Label"
top={10}
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
@ -201,12 +283,19 @@ export const MiniProfilePopup = () => {
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<PopupMenu
noHeader
title="Due Date"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>
</PopupMenu>
)}
@ -236,3 +325,4 @@ export const MiniProfilePopup = () => {
</>
);
};

View File

@ -1,20 +1,34 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{ top: number; left: number; ref: any }>`
export const Container = styled.div<{ invert: boolean; top: number; left: number; ref: any }>`
left: ${props => props.left}px;
top: ${props => props.top}px;
background: #fff;
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
display: block;
position: absolute;
width: 304px;
z-index: 100000000000;
&:focus {
outline: none;
border: none;
}
width: 316px;
padding-top: 10px;
height: auto;
z-index: 40000;
${props =>
props.invert &&
css`
transform: translate(-100%);
`}
`;
export const Wrapper = styled.div`
padding: 5px;
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
export const Header = styled.div`
@ -26,10 +40,10 @@ export const Header = styled.div`
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #5e6c84;
color: #c2c6dc;
display: block;
line-height: 40px;
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
border-bottom: 1px solid #414561;
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
@ -46,23 +60,30 @@ export const Content = styled.div`
padding: 0 12px 12px;
`;
export const LabelSearch = styled.input`
box-sizing: border-box;
display: block;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
display: block;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const Section = styled.div`
@ -70,7 +91,7 @@ export const Section = styled.div`
`;
export const SectionTitle = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -95,7 +116,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
props.active &&
css`
margin-left: 4px;
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
border-radius: 3px;
`}
@ -113,6 +134,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 31px;
`;
export const CloseButton = styled.div`
@ -126,8 +148,6 @@ export const CloseButton = styled.div`
align-items: center;
justify-content: center;
z-index: 40;
height: 20px;
width: 20px;
cursor: pointer;
`;
@ -142,14 +162,14 @@ export const LabelIcon = styled.div`
align-items: center;
justify-content: center;
height: 20px;
height: 100%;
font-size: 16px;
line-height: 20px;
width: 20px;
width: auto;
cursor: pointer;
&:hover {
background: rgba(9, 30, 66, 0.08);
background: rgb(115, 103, 240);
}
`;
@ -186,19 +206,27 @@ export const FieldLabel = styled.label`
export const FieldName = styled.input`
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border-radius: 3px;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
font-size: 12px;
font-weight: 400;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const LabelBox = styled.span<{ color: string }>`
@ -208,6 +236,7 @@ export const LabelBox = styled.span<{ color: string }>`
padding: 0;
width: 48px;
cursor: pointer;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
@ -217,6 +246,7 @@ export const LabelBox = styled.span<{ color: string }>`
`;
export const SaveButton = styled.input`
cursor: pointer;
background-color: #5aac44;
box-shadow: none;
border: none;
@ -239,8 +269,7 @@ export const DeleteButton = styled.input`
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
type="submit"font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
@ -248,3 +277,53 @@ export const DeleteButton = styled.input`
border-radius: 3px;
float: right;
`;
export const CreateLabelButton = styled.button`
outline: none;
border: none;
width: 100%;
border-radius: 3px;
line-height: 20px;
margin-bottom: 8px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
margin: 8px 4px 0 0;
font-size: 14px;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const PreviousButton = styled.div`
padding: 10px 12px 10px 8px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
cursor: pointer;
`;
export const ContainerDiamond = styled.div<{ invert: boolean }>`
top: 10px;
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute;
width: 10px;
height: 10px;
display: block;
transform: rotate(45deg) translate(-7px);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
z-index: 10;
background: #262c49;
border-color: #414561;
`;

View File

@ -1,30 +1,219 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Container, Header, HeaderTitle, Content, CloseButton } from './Styles';
import { createPortal } from 'react-dom';
import produce from 'immer';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
type Props = {
title: string;
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element) => void;
setTab: (newTab: number) => void;
getCurrentTab: () => number;
};
type PopupProps = {
title: string | null;
onClose: () => void;
tab: number;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
onClose: () => void;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, children }) => {
const PopupContainer: React.FC<PopupContainerProps> = ({ top, left, onClose, children, invert }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef} invert={invert}>
{children}
</Container>
);
};
const PopupContext = createContext<PopupContextState>({
show: () => {},
setTab: () => {},
getCurrentTab: () => 0,
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab };
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
currentTab: 0,
previousTab: 0,
content: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element) => {
console.log(target);
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
if (bounds.left + 304 + 30 > window.innerWidth) {
console.log('open!');
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
invert: true,
currentTab: 0,
previousTab: 0,
content,
});
} else {
console.log('open NOT INVERT!');
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
invert: false,
currentTab: 0,
previousTab: 0,
content,
});
}
}
};
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number) => {
setState((prevState: PopupState) => {
return {
...prevState,
previousTab: currentState.currentTab,
currentTab: newTab,
};
});
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ show, setTab, getCurrentTab }}>
{portalTarget &&
currentState.isOpen &&
createPortal(
<PopupContainer
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
onClose={() => setState(defaultState)}
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
type Props = {
title: string | null;
top: number;
left: number;
onClose: () => void;
onPrevious?: () => void | null;
noHeader?: boolean | null;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef}>
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross />
</CloseButton>
</Header>
<Content>{children}</Content>
<Container invert={false} left={left} top={top} ref={$containerRef}>
<Wrapper>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
<Content>{children}</Content>
</Wrapper>
</>
);
};
export default PopupMenu;