initial commit

This commit is contained in:
Jordan Knott
2020-04-09 21:40:22 -05:00
commit 9611105364
141 changed files with 29236 additions and 0 deletions

View File

@ -0,0 +1,116 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import Card from './index';
export default {
component: Card,
title: 'Card',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
active: false,
},
];
export const Default = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
cardId="1"
listId="1"
description=""
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
/>
);
};
export const Labels = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
cardId="1"
listId="1"
description=""
ref={$ref}
title="Hello, world"
labels={labelData}
onClick={action('on click')}
onContextMenu={action('on context click')}
/>
);
};
export const Badges = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
cardId="1"
listId="1"
description="hello!"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
);
};
export const PastDue = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
cardId="1"
listId="1"
description="hello!"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: true, formattedDate: 'Oct 26, 2020' }}
/>
);
};
export const Everything = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
cardId="1"
listId="1"
description="hello!"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
);
};

View File

@ -0,0 +1,122 @@
import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
export const ClockIcon = styled(FontAwesomeIcon)``;
export const ListCardBadges = styled.div`
float: left;
display: flex;
max-width: 100%;
margin-left: -2px;
`;
export const ListCardBadge = styled.div`
color: #5e6c84;
display: flex;
align-items: center;
margin: 0 6px 4px 0;
max-width: 100%;
min-height: 20px;
overflow: hidden;
position: relative;
padding: 2px;
text-decoration: none;
text-overflow: ellipsis;
vertical-align: top;
`;
export const DescriptionBadge = styled(ListCardBadge)`
padding-right: 6px;
`;
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
${props =>
props.isPastDue &&
css`
padding-left: 4px;
background-color: #ec9488;
border-radius: 3px;
color: #fff;
`}
`;
export const ListCardBadgeText = styled.span`
font-size: 12px;
padding: 0 4px 0 6px;
vertical-align: top;
white-space: nowrap;
`;
export const ListCardContainer = styled.div<{ isActive: boolean }>`
max-width: 256px;
margin-bottom: 8px;
background-color: #fff;
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer !important;
position: relative;
background-color: ${props => (props.isActive ? mixin.darken('#262c49', 0.1) : mixin.lighten('#262c49', 0.05))};
`;
export const ListCardInnerContainer = styled.div`
width: 100%;
height: 100%;
`;
export const ListCardDetails = styled.div`
overflow: hidden;
padding: 6px 8px 2px;
position: relative;
z-index: 10;
`;
export const ListCardLabels = styled.div`
overflow: auto;
position: relative;
`;
export const ListCardLabel = styled.span`
height: 16px;
line-height: 16px;
padding: 0 8px;
max-width: 198px;
float: left;
font-size: 12px;
font-weight: 700;
margin: 0 4px 4px 0;
width: auto;
border-radius: 4px;
color: #fff;
display: block;
position: relative;
background-color: ${props => props.color};
`;
export const ListCardOperation = styled.span`
display: flex;
align-content: center;
justify-content: center;
background-color: ${props => mixin.darken('#262c49', 0.15)};
background-clip: padding-box;
background-origin: padding-box;
border-radius: 3px;
opacity: 0.8;
padding: 6px;
position: absolute;
right: 2px;
top: 2px;
z-index: 10;
`;
export const CardTitle = styled.span`
font-family: 'Droid Sans';
clear: both;
display: block;
margin: 0 0 4px;
overflow: hidden;
text-decoration: none;
word-wrap: break-word;
color: #c2c6dc;
`;

View File

@ -0,0 +1,144 @@
import React, { useState, useRef } from 'react';
import { DraggableProvidedDraggableProps } from 'react-beautiful-dnd';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
import { faClock, faCheckSquare, faEye } from '@fortawesome/free-regular-svg-icons';
import {
DescriptionBadge,
DueDateCardBadge,
ListCardBadges,
ListCardBadge,
ListCardBadgeText,
ListCardContainer,
ListCardInnerContainer,
ListCardDetails,
ClockIcon,
ListCardLabels,
ListCardLabel,
ListCardOperation,
CardTitle,
} from './Styles';
type DueDate = {
isPastDue: boolean;
formattedDate: string;
};
type Checklist = {
complete: number;
total: number;
};
type Props = {
title: string;
description: string;
cardId: string;
listId: string;
onContextMenu: (e: ContextMenuEvent) => void;
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
dueDate?: DueDate;
checklists?: Checklist;
watched?: boolean;
labels?: Label[];
wrapperProps?: any;
};
const Card = React.forwardRef(
(
{
wrapperProps,
onContextMenu,
cardId,
listId,
onClick,
labels,
title,
dueDate,
description,
checklists,
watched,
}: Props,
$cardRef: any,
) => {
const [isActive, setActive] = useState(false);
const $innerCardRef: any = useRef(null);
const onOpenComposer = () => {
if (typeof $innerCardRef.current !== 'undefined') {
const pos = $innerCardRef.current.getBoundingClientRect();
onContextMenu({
top: pos.top,
left: pos.left,
listId,
cardId,
});
}
};
const onTaskContext = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onOpenComposer();
};
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault();
e.stopPropagation();
onOpenComposer();
};
return (
<ListCardContainer
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
ref={$cardRef}
onClick={onClick}
onContextMenu={onTaskContext}
isActive={isActive}
{...wrapperProps}
>
<ListCardInnerContainer ref={$innerCardRef}>
<ListCardOperation>
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
</ListCardOperation>
<ListCardDetails>
<ListCardLabels>
{labels &&
labels.map(label => (
<ListCardLabel color={label.color} key={label.name}>
{label.name}
</ListCardLabel>
))}
</ListCardLabels>
<CardTitle>{title}</CardTitle>
<ListCardBadges>
{watched && (
<ListCardBadge>
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
</ListCardBadge>
)}
{dueDate && (
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
</DueDateCardBadge>
)}
{description && (
<DescriptionBadge>
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
</DescriptionBadge>
)}
{checklists && (
<ListCardBadge>
<FontAwesomeIcon color="#6b778c" icon={faCheckSquare} size="xs" />
<ListCardBadgeText>{`${checklists.complete}/${checklists.total}`}</ListCardBadgeText>
</ListCardBadge>
)}
</ListCardBadges>
</ListCardDetails>
</ListCardInnerContainer>
</ListCardContainer>
);
},
);
Card.displayName = 'Card';
export default Card;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import CardComposer from './index';
export default {
component: CardComposer,
title: 'CardComposer',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return <CardComposer isOpen onClose={action('on close')} onCreateCard={action('on create card')} />;
};

View File

@ -0,0 +1,89 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
export const CancelIcon = styled(FontAwesomeIcon)`
opacity: 0.8;
cursor: pointer;
font-size: 1.25em;
padding-left: 5px;
`;
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
padding-bottom: 8px;
display: ${props => (props.isOpen ? 'flex' : 'none')};
flex-direction: column;
`;
export const ListCard = styled.div`
background-color: #fff;
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer;
display: block;
margin-bottom: 8px;
max-width: 300px;
min-height: 20px;
position: relative;
text-decoration: none;
z-index: 0;
`;
export const ListCardDetails = styled.div`
overflow: hidden;
padding: 6px 8px 2px;
position: relative;
z-index: 10;
`;
export const ListCardLabels = styled.div``;
export const ListCardEditor = styled(TextareaAutosize)`
font-family: 'Droid Sans';
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
background: none;
border: none;
box-shadow: none;
margin-bottom: 4px;
max-height: 162px;
min-height: 54px;
padding: 0;
font-size: 14px;
line-height: 20px;
&:focus {
border: none;
outline: none;
}
`;
export const ComposerControls = styled.div``;
export const ComposerControlsSaveSection = styled.div`
display: flex;
float: left;
align-items: center;
justify-content: center;
`;
export const ComposerControlsActionsSection = styled.div`
float: right;
`;
export const AddCardButton = styled.button`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
margin-right: 4px;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
`;

View File

@ -0,0 +1,85 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import TextareaAutosize from 'react-autosize-textarea';
import {
CardComposerWrapper,
CancelIcon,
AddCardButton,
ListCard,
ListCardDetails,
ListCardEditor,
ComposerControls,
ComposerControlsSaveSection,
ComposerControlsActionsSection,
} from './Styles';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
type Props = {
isOpen: boolean;
onCreateCard: (cardName: string) => void;
onClose: () => void;
};
const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
const [cardName, setCardName] = useState('');
const $cardEditor: any = useRef(null);
const onClick = () => {
onCreateCard(cardName);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
onCreateCard(cardName);
}
};
const onBlur = () => {
if (cardName === '') {
onClose();
} else {
onCreateCard(cardName);
}
};
useOnEscapeKeyDown(isOpen, onClose);
useOnOutsideClick($cardEditor, true, () => onClose(), null);
useEffect(() => {
$cardEditor.current.focus();
}, []);
return (
<CardComposerWrapper isOpen={isOpen}>
<ListCard>
<ListCardDetails>
<ListCardEditor
onKeyDown={onKeyDown}
ref={$cardEditor}
onChange={e => {
setCardName(e.currentTarget.value);
}}
value={cardName}
placeholder="Enter a title for this card..."
/>
</ListCardDetails>
</ListCard>
<ComposerControls>
<ComposerControlsSaveSection>
<AddCardButton onClick={onClick}>Add Card</AddCardButton>
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
</ComposerControlsSaveSection>
<ComposerControlsActionsSection />
</ComposerControls>
</CardComposerWrapper>
);
};
CardComposer.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onCreateCard: PropTypes.func.isRequired,
};
CardComposer.defaultProps = {
isOpen: true,
};
export default CardComposer;

View File

@ -0,0 +1,56 @@
import React, { createRef, useState } from 'react';
import styled from 'styled-components';
import { action } from '@storybook/addon-actions';
import DropdownMenu from './index';
export default {
component: DropdownMenu,
title: 'DropdownMenu',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#f8f8f8' },
{ name: 'darkBlue', value: '#262c49', default: true },
],
},
};
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const Button = styled.div`
font-size: 18px;
padding: 15px 20px;
color: #fff;
background: #000;
`;
export const Default = () => {
const [menu, setMenu] = useState({
top: 0,
left: 0,
isOpen: false,
});
const $buttonRef: any = createRef();
const onClick = () => {
console.log($buttonRef.current.getBoundingClientRect());
setMenu({
isOpen: !menu.isOpen,
left: $buttonRef.current.getBoundingClientRect().right,
top: $buttonRef.current.getBoundingClientRect().bottom,
});
};
return (
<>
<Container>
<Button onClick={onClick} ref={$buttonRef}>
Click me
</Button>
</Container>
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
</>
);
};

View File

@ -0,0 +1,74 @@
import styled from 'styled-components/macro';
export const Container = styled.div<{ left: number; top: number }>`
position: absolute;
left: ${props => props.left}px;
top: ${props => props.top}px;
padding-top: 10px;
position: absolute;
height: auto;
width: auto;
transform: translate(-100%);
transition: opacity 0.25s, transform 0.25s, width 0.3s ease;
z-index: 40000;
`;
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);
border: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border-color: #414561;
`;
export const WrapperDiamond = styled.div`
top: 10px;
right: 10px;
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;
`;
export const ActionsList = styled.ul`
min-width: 9rem;
margin: 0;
padding: 0;
`;
export const ActionItem = styled.li`
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem !important;
padding-bottom: 0.5rem !important;
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const ActionTitle = styled.span`
margin-left: 0.5rem;
`;
export const Separator = styled.div`
height: 1px;
border-top: 1px solid #414561;
margin: 0.25rem !important;
`;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Exit, User } from 'shared/icons';
import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem, ActionTitle } from './Styles';
type DropdownMenuProps = {
left: number;
top: number;
};
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => {
return (
<Container left={left} top={top}>
<Wrapper>
<ActionItem>
<User size={16} color="#c2c6dc" />
<ActionTitle>Profile</ActionTitle>
</ActionItem>
<Separator />
<ActionsList>
<ActionItem>
<Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle>
</ActionItem>
</ActionsList>
</Wrapper>
<WrapperDiamond />
</Container>
);
};
export default DropdownMenu;

View File

@ -0,0 +1,178 @@
import React, { createRef } from 'react';
import { action } from '@storybook/addon-actions';
import Card from 'shared/components/Card';
import CardComposer from 'shared/components/CardComposer';
import LabelColors from 'shared/constants/labelColors';
import List, { ListCards } from './index';
export default {
component: List,
title: 'List',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
active: false,
},
];
const createCard = () => {
const $ref = createRef<HTMLDivElement>();
return (
<Card
cardId="1"
listId="1"
description="hello!"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
);
};
export const Default = () => {
return (
<List
id=""
name="General"
isComposerOpen={false}
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
>
<ListCards>
<CardComposer
onClose={() => {
console.log('close!');
}}
onCreateCard={name => {
console.log(name);
}}
isOpen={false}
/>
</ListCards>
</List>
);
};
export const WithCardComposer = () => {
return (
<List
id="1"
name="General"
isComposerOpen
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
>
<ListCards>
<CardComposer
onClose={() => {
console.log('close!');
}}
onCreateCard={name => {
console.log(name);
}}
isOpen
/>
</ListCards>
</List>
);
};
export const WithCard = () => {
const $cardRef: any = createRef();
return (
<List
id="1"
name="General"
isComposerOpen={false}
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
>
<ListCards>
<Card
cardId="1"
listId="1"
description="hello!"
ref={$cardRef}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
<CardComposer
onClose={() => {
console.log('close!');
}}
onCreateCard={name => {
console.log(name);
}}
isOpen={false}
/>
</ListCards>
</List>
);
};
export const WithCardAndComposer = () => {
const $cardRef: any = createRef();
return (
<List
id="1"
name="General"
isComposerOpen
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
>
<ListCards>
<Card
cardId="1"
listId="1"
description="hello!"
ref={$cardRef}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
<CardComposer
onClose={() => {
console.log('close!');
}}
onCreateCard={name => {
console.log(name);
}}
isOpen
/>
</ListCards>
</List>
);
};

View File

@ -0,0 +1,119 @@
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div`
width: 272px;
margin: 0 4px;
height: 100%;
box-sizing: border-box;
display: inline-block;
vertical-align: top;
white-space: nowrap;
`;
export const AddCardContainer = styled.div`
min-height: 38px;
max-height: 38px;
display: ${props => (props.hidden ? 'none' : 'flex')};
justify-content: space-between;
`;
export const AddCardButton = styled.a`
border-radius: 3px;
color: #5e6c84;
display: flex;
align-items: center;
cursor: pointer;
flex: 1 0 auto;
margin: 2px 8px 8px 8px;
padding: 4px 8px;
position: relative;
text-decoration: none;
user-select: none;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
color: #172b4d;
text-decoration: none;
}
`;
export const Wrapper = styled.div`
// background-color: #ebecf0;
// background: rgb(244, 245, 247);
background: #10163a;
color: #c2c6dc;
border-radius: 5px;
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 100%;
position: relative;
white-space: normal;
`;
export const HeaderEditTarget = styled.div<{ isHidden: boolean }>`
cursor: pointer;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: ${props => (props.isHidden ? 'none' : 'block')};
`;
export const HeaderName = styled(TextareaAutosize)`
font-family: 'Droid Sans';
border: none;
resize: none;
overflow: hidden;
overflow-wrap: break-word;
background: transparent;
border-radius: 3px;
box-shadow: none;
font-weight: 600;
margin: -4px 0;
padding: 4px 8px;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
flex-direction: column;
text-align: start;
color: #c2c6dc;
`;
export const Header = styled.div<{ isEditing: boolean }>`
flex: 0 0 auto;
padding: 10px 8px;
position: relative;
min-height: 20px;
padding-right: 36px;
${props =>
props.isEditing &&
css`
& ${HeaderName} {
background: #fff;
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
}
`}
`;
export const AddCardButtonText = styled.span`
padding-left: 5px;
font-family: 'Droid Sans';
`;
export const ListCards = styled.div`
margin: 0 4px;
padding: 0 4px;
flex: 1 1 auto;
min-height: 30px;
overflow-y: auto;
overflow-x: hidden;
`;

View File

@ -0,0 +1,101 @@
import React, { useState, useRef } from 'react';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import {
Container,
Wrapper,
Header,
HeaderName,
HeaderEditTarget,
AddCardContainer,
AddCardButton,
AddCardButtonText,
ListCards,
} from './Styles';
type Props = {
children: React.ReactNode;
id: string;
name: string;
onSaveName: (name: string) => void;
isComposerOpen: boolean;
onOpenComposer: (id: string) => void;
tasks: Task[];
wrapperProps?: any;
headerProps?: any;
index?: number;
};
const List = React.forwardRef(
(
{ id, name, onSaveName, isComposerOpen, onOpenComposer, children, wrapperProps, headerProps }: Props,
$wrapperRef: any,
) => {
const [listName, setListName] = useState(name);
const [isEditingTitle, setEditingTitle] = useState(false);
const $listNameRef: any = useRef<HTMLTextAreaElement>();
const onClick = () => {
setEditingTitle(true);
if ($listNameRef) {
$listNameRef.current.select();
}
};
const onBlur = () => {
setEditingTitle(false);
onSaveName(listName);
};
const onEscape = () => {
$listNameRef.current.blur();
};
const onChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
setListName(event.currentTarget.value);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
$listNameRef.current.blur();
}
};
useOnEscapeKeyDown(isEditingTitle, onEscape);
return (
<Container ref={$wrapperRef} {...wrapperProps}>
<Wrapper>
<Header {...headerProps} isEditing={isEditingTitle}>
<HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} />
<HeaderName
ref={$listNameRef}
onBlur={onBlur}
onChange={onChange}
onKeyDown={onKeyDown}
spellCheck={false}
value={listName}
/>
</Header>
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<FontAwesomeIcon icon={faPlus} size="xs" color="#42526e" />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>
</Wrapper>
</Container>
);
},
);
List.defaultProps = {
children: null,
isComposerOpen: false,
wrapperProps: {},
headerProps: {},
};
List.displayName = 'List';
export default List;
export { ListCards };

View File

@ -0,0 +1,184 @@
import React, { useState } from 'react';
import Lists from './index';
import { action } from '@storybook/addon-actions';
export default {
component: Lists,
title: 'Lists',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const initialListsData = {
columns: {
'column-1': {
taskGroupID: 'column-1',
name: 'General',
taskIds: ['task-3', 'task-4', 'task-1', 'task-2'],
position: 1,
tasks: [],
},
'column-2': {
taskGroupID: 'column-2',
name: 'Development',
taskIds: [],
position: 2,
tasks: [],
},
},
tasks: {
'task-1': {
taskID: 'task-1',
taskGroupID: 'column-1',
name: 'Create roadmap',
position: 2,
labels: [],
},
'task-2': {
taskID: 'task-2',
taskGroupID: 'column-1',
position: 1,
name: 'Create authentication',
labels: [],
},
'task-3': {
taskID: 'task-3',
taskGroupID: 'column-1',
position: 3,
name: 'Create login',
labels: [],
},
'task-4': {
taskID: 'task-4',
taskGroupID: 'column-1',
position: 4,
name: 'Create plugins',
labels: [],
},
},
};
export const Default = () => {
const [listsData, setListsData] = useState(initialListsData);
const onCardDrop = (droppedTask: any) => {
console.log(droppedTask);
const newState = {
...listsData,
tasks: {
...listsData.tasks,
[droppedTask.id]: droppedTask,
},
};
console.log(newState);
setListsData(newState);
};
const onListDrop = (droppedColumn: any) => {
const newState = {
...listsData,
columns: {
...listsData.columns,
[droppedColumn.id]: droppedColumn,
},
};
setListsData(newState);
};
return (
<Lists
{...listsData}
onQuickEditorOpen={action('card composer open')}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={action('card create')}
/>
);
};
const createColumn = (id: any, name: any, position: any) => {
return {
taskGroupID: id,
name,
position,
tasks: [],
};
};
const initialListsDataLarge = {
columns: {
'column-1': createColumn('column-1', 'General', 1),
'column-2': createColumn('column-2', 'General', 2),
'column-3': createColumn('column-3', 'General', 3),
'column-4': createColumn('column-4', 'General', 4),
'column-5': createColumn('column-5', 'General', 5),
'column-6': createColumn('column-6', 'General', 6),
'column-7': createColumn('column-7', 'General', 7),
'column-8': createColumn('column-8', 'General', 8),
'column-9': createColumn('column-9', 'General', 9),
},
tasks: {
'task-1': {
taskID: 'task-1',
taskGroupID: 'column-1',
name: 'Create roadmap',
position: 2,
labels: [],
},
'task-2': {
taskID: 'task-2',
taskGroupID: 'column-1',
position: 1,
name: 'Create authentication',
labels: [],
},
'task-3': {
taskID: 'task-3',
taskGroupID: 'column-1',
position: 3,
name: 'Create login',
labels: [],
},
'task-4': {
taskID: 'task-4',
taskGroupID: 'column-1',
position: 4,
name: 'Create plugins',
labels: [],
},
},
};
export const ListsWithManyList = () => {
const [listsData, setListsData] = useState(initialListsDataLarge);
const onCardDrop = (droppedTask: any) => {
const newState = {
...listsData,
tasks: {
...listsData.tasks,
[droppedTask.id]: droppedTask,
},
};
setListsData(newState);
};
const onListDrop = (droppedColumn: any) => {
const newState = {
...listsData,
columns: {
...listsData.columns,
[droppedColumn.id]: droppedColumn,
},
};
setListsData(newState);
};
return (
<Lists
{...listsData}
onQuickEditorOpen={action('card composer open')}
onCardCreate={action('card create')}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
/>
);
};

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
export const Container = styled.div`
flex-grow: 1;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
`;

View File

@ -0,0 +1,196 @@
import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/arrays';
import List, { ListCards } from 'shared/components/List';
import Card from 'shared/components/Card';
import { Container } from './Styles';
import CardComposer from 'shared/components/CardComposer';
const getNewDraggablePosition = (afterDropDraggables: any, draggableIndex: any) => {
const prevDraggable = afterDropDraggables[draggableIndex - 1];
const nextDraggable = afterDropDraggables[draggableIndex + 1];
if (!prevDraggable && !nextDraggable) {
return 1;
}
if (!prevDraggable) {
return nextDraggable.position - 1;
}
if (!nextDraggable) {
return prevDraggable.position + 1;
}
const newPos = (prevDraggable.position + nextDraggable.position) / 2.0;
return newPos;
};
const getSortedDraggables = (draggables: any) => {
return draggables.sort((a: any, b: any) => a.position - b.position);
};
const isPositionChanged = (source: any, destination: any) => {
if (!destination) return false;
const isSameList = destination.droppableId === source.droppableId;
const isSamePosition = destination.index === source.index;
return !isSameList || !isSamePosition;
};
const getAfterDropDraggableList = (
beforeDropDraggables: any,
droppedDraggable: any,
isList: any,
isSameList: any,
destination: any,
) => {
if (isList) {
return moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index);
}
return isSameList
? moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index)
: insertItemIntoArray(beforeDropDraggables, droppedDraggable, destination.index);
};
interface Columns {
[key: string]: TaskGroup;
}
interface Tasks {
[key: string]: RemoteTask;
}
type Props = {
columns: Columns;
tasks: Tasks;
onCardDrop: any;
onListDrop: any;
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
};
type OnDragEndProps = {
draggableId: any;
source: any;
destination: any;
type: any;
};
const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEditorOpen }: Props) => {
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return;
const isList = type === 'column';
const isSameList = destination.droppableId === source.droppableId;
const droppedDraggable = isList ? columns[draggableId] : tasks[draggableId];
const beforeDropDraggables = isList
? getSortedDraggables(Object.values(columns))
: getSortedDraggables(Object.values(tasks).filter((t: any) => t.taskGroupID === destination.droppableId));
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
isList,
isSameList,
destination,
);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
if (isList) {
onListDrop({
...droppedDraggable,
position: newPosition,
});
} else {
const newCard = {
...droppedDraggable,
position: newPosition,
taskGroupID: destination.droppableId,
};
onCardDrop(newCard);
}
};
const orderedColumns = getSortedDraggables(Object.values(columns));
const [currentComposer, setCurrentComposer] = useState('');
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root">
{provided => (
<Container {...provided.droppableProps} ref={provided.innerRef}>
{orderedColumns.map((column: TaskGroup, index: number) => {
const columnCards = getSortedDraggables(
Object.values(tasks).filter((t: any) => t.taskGroupID === column.taskGroupID),
);
return (
<Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}>
{columnDragProvided => (
<List
id={column.taskGroupID}
name={column.name}
key={column.taskGroupID}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === column.taskGroupID}
onSaveName={name => console.log(name)}
index={index}
tasks={columnCards}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
>
<Droppable type="tasks" droppableId={column.taskGroupID}>
{columnDropProvided => (
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{columnCards.map((task: RemoteTask, taskIndex: any) => {
return (
<Draggable key={task.taskID} draggableId={task.taskID} index={taskIndex}>
{taskProvided => {
return (
<Card
wrapperProps={{
...taskProvided.draggableProps,
...taskProvided.dragHandleProps,
}}
ref={taskProvided.innerRef}
cardId={task.taskID}
listId={column.taskGroupID}
description=""
title={task.name}
labels={task.labels}
onClick={e => console.log(e)}
onContextMenu={onQuickEditorOpen}
/>
);
}}
</Draggable>
);
})}
{columnDropProvided.placeholder}
{currentComposer === column.taskGroupID && (
<CardComposer
onClose={() => {
setCurrentComposer('');
}}
onCreateCard={name => {
setCurrentComposer('');
onCardCreate(column.taskGroupID, name);
}}
isOpen={true}
/>
)}
</ListCards>
)}
</Droppable>
</List>
)}
</Draggable>
);
})}
{provided.placeholder}
</Container>
)}
</Droppable>
</DragDropContext>
);
};
export default Lists;

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import styled from 'styled-components';
import Login from './index';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export default {
component: Login,
title: 'Login',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
`;
const LoginWrapper = styled.div`
width: 60%;
`;
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Container>
<LoginWrapper>
<Login onSubmit={action('on submit')} />
</LoginWrapper>
</Container>
</>
);
};
export const WithSubmission = () => {
const onSubmit = async (data: LoginFormData, setComplete: (val: boolean) => void, setError: any) => {
await sleep(2000);
if (data.username !== 'test' || data.password !== 'test') {
setError('username', 'invalid', 'Invalid username');
setError('password', 'invalid', 'Invalid password');
}
setComplete(true);
};
return (
<>
<NormalizeStyles />
<BaseStyles />
<Container>
<LoginWrapper>
<Login onSubmit={onSubmit} />
</LoginWrapper>
</Container>
</>
);
};

View File

@ -0,0 +1,103 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
background: #eff2f7;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
export const Column = styled.div`
width: 50%;
display: flex;
justify-content: center;
align-items: center;
`;
export const LoginFormWrapper = styled.div`
background: #10163a;
width: 100%;
`;
export const LoginFormContainer = styled.div`
min-height: 505px;
padding: 2rem;
`;
export const Title = styled.h1`
color: #ebeefd;
font-size: 18px;
margin-bottom: 14px;
`;
export const SubTitle = styled.h2`
color: #c2c6dc;
font-size: 14px;
margin-bottom: 14px;
`;
export const Form = styled.form`
display: flex;
flex-direction: column;
`;
export const FormLabel = styled.label`
color: #c2c6dc;
font-size: 12px;
position: relative;
margin-top: 14px;
`;
export const FormTextInput = styled.input`
width: 100%;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.2);
margin-top: 4px;
padding: 0.7rem 1rem 0.7rem 3rem;
font-size: 1rem;
color: #c2c6dc;
border-radius: 5px;
`;
export const FormIcon = styled.div`
top: 30px;
left: 16px;
position: absolute;
`;
export const FormError = styled.span`
font-size: 0.875rem;
color: rgb(234, 84, 85);
`;
export const LoginButton = styled.input`
padding: 0.75rem 2rem;
font-size: 1rem;
border-radius: 6px;
background: rgb(115, 103, 240);
outline: none;
border: none;
cursor: pointer;
color: #fff;
&:disabled {
opacity: 0.5;
cursor: default;
pointer-events: none;
}
`;
export const ActionButtons = styled.div`
margin-top: 17.5px;
display: flex;
justify-content: space-between;
`;
export const RegisterButton = styled.button`
padding: 0.679rem 2rem;
border-radius: 6px;
border: 1px solid rgb(115, 103, 240);
background: transparent;
font-size: 1rem;
color: rgba(115, 103, 240);
cursor: pointer;
`;

View File

@ -0,0 +1,81 @@
import React, { useState } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock } from 'shared/icons';
import { useForm } from 'react-hook-form';
import {
Form,
ActionButtons,
RegisterButton,
LoginButton,
FormError,
FormIcon,
FormLabel,
FormTextInput,
Wrapper,
Column,
LoginFormWrapper,
LoginFormContainer,
Title,
SubTitle,
} from './Styles';
const Login = ({ onSubmit }: LoginProps) => {
const [isComplete, setComplete] = useState(true);
const { register, handleSubmit, errors, setError, formState } = useForm<LoginFormData>();
console.log(formState);
const loginSubmit = (data: LoginFormData) => {
setComplete(false);
onSubmit(data, setComplete, setError);
};
return (
<Wrapper>
<Column>
<AccessAccount width={275} height={250} />
</Column>
<Column>
<LoginFormWrapper>
<LoginFormContainer>
<Title>Login</Title>
<SubTitle>Welcome back, please login into your account.</SubTitle>
<Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="username">
Username
<FormTextInput
type="text"
id="username"
name="username"
ref={register({ required: 'Username is required' })}
/>
<FormIcon>
<User color="#c2c6dc" size={20} />
</FormIcon>
</FormLabel>
{errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="password">
Password
<FormTextInput
type="text"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon>
<Lock color="#c2c6dc" size={20} />
</FormIcon>
</FormLabel>
{errors.password && <FormError>{errors.password.message}</FormError>}
<ActionButtons>
<RegisterButton>Register</RegisterButton>
<LoginButton type="submit" value="Login" disabled={!isComplete} />
</ActionButtons>
</Form>
</LoginFormContainer>
</LoginFormWrapper>
</Column>
</Wrapper>
);
};
export default Login;

View File

@ -0,0 +1,41 @@
import React from 'react';
import styled from 'styled-components';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import { Home, Stack, Users, Question } from 'shared/icons';
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from './index';
export default {
component: Navbar,
title: 'Navbar',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#cdd3e1' },
],
},
};
const MainContent = styled.div`
padding: 0 0 50px 80px;
`;
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Navbar>
<PrimaryLogo />
<ButtonContainer>
<ActionButton name="Home">
<Home size={28} color="#c2c6dc" />
</ActionButton>
<ActionButton name="Home">
<Home size={28} color="#c2c6dc" />
</ActionButton>
</ButtonContainer>
</Navbar>
</>
);
};

View File

@ -0,0 +1,105 @@
import styled, { css } from 'styled-components';
export const LogoWrapper = styled.div`
margin: 20px 0px 20px;
position: relative;
width: 100%;
height: 42px;
line-height: 42px;
padding-left: 64px;
color: rgb(222, 235, 255);
cursor: pointer;
user-select: none;
transition: color 0.1s ease 0s;
`;
export const Logo = styled.div`
position: absolute;
left: 19px;
`;
export const LogoTitle = styled.div`
position: relative;
right: 12px;
visibility: hidden;
opacity: 0;
font-size: 24px;
font-weight: 600;
transition: right 0.1s ease 0s, visibility, opacity, transform 0.25s ease;
color: #7367f0;
`;
export const ActionContainer = styled.div`
position: relative;
`;
export const ActionButtonTitle = styled.span`
position: relative;
visibility: hidden;
left: -5px;
opacity: 0;
font-weight: 600;
transition: left 0.1s ease 0s, visibility, opacity, transform 0.25s ease;
font-size: 18px;
color: #c2c6dc;
`;
export const IconWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease;
`;
export const ActionButtonContainer = styled.div`
padding: 0 12px;
position: relative;
`;
export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
${props =>
props.active &&
css`
background: rgb(115, 103, 240);
box-shadow: 0 0 10px 1px rgba(115, 103, 240, 0.7);
`}
border-radius: 6px;
cursor: pointer;
padding: 10px 15px;
display: flex;
align-items: center;
&:hover ${ActionButtonTitle} {
transform: translateX(5px);
}
&:hover ${IconWrapper} {
transform: translateX(5px);
}
`;
export const Container = styled.aside`
z-index: 100;
position: fixed;
top: 0px;
left: 0px;
overflow-x: hidden;
height: 100vh;
width: 80px;
transform: translateZ(0px);
background: #10163a;
transition: all 0.1s ease 0s;
&:hover {
width: 260px;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
}
&:hover ${LogoTitle} {
right: 0px;
visibility: visible;
opacity: 1;
}
&:hover ${ActionButtonTitle} {
left: 15px;
visibility: visible;
opacity: 1;
}
`;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Citadel } from 'shared/icons';
import {
Container,
LogoWrapper,
IconWrapper,
Logo,
LogoTitle,
ActionContainer,
ActionButtonContainer,
ActionButtonWrapper,
ActionButtonTitle,
} from './Styles';
type ActionButtonProps = {
name: string;
active?: boolean;
};
export const ActionButton: React.FC<ActionButtonProps> = ({ name, active, children }) => {
return (
<ActionButtonWrapper active={active ?? false}>
<IconWrapper>{children}</IconWrapper>
<ActionButtonTitle>{name}</ActionButtonTitle>
</ActionButtonWrapper>
);
};
export const ButtonContainer: React.FC = ({ children }) => (
<ActionContainer>
<ActionButtonContainer>{children}</ActionButtonContainer>
</ActionContainer>
);
export const PrimaryLogo = () => {
return (
<LogoWrapper>
<Logo>
<Citadel size={42} />
</Logo>
<LogoTitle>Citadel</LogoTitle>
</LogoWrapper>
);
};
const Navbar: React.FC = ({ children }) => {
return <Container>{children}</Container>;
};
export default Navbar;

View File

@ -0,0 +1,31 @@
import React, { useState } from 'react';
import LabelColors from 'shared/constants/labelColors';
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;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
<LabelBox color={labelColor}>
<Checkmark color="#fff" size={12} />
</LabelBox>
))}
</div>
<div>
<SaveButton type="submit" value="Save" />
<DeleteButton type="submit" value="Delete" />
</div>
</EditLabelForm>
);
};
export default LabelManager;

View File

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
type Props = {
labels?: Label[];
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
};
const LabelManager = ({ labels, onLabelToggle, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
return (
<>
<LabelSearch type="text" />
<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>
</Section>
</>
);
};
export default LabelManager;

View File

@ -0,0 +1,76 @@
import React, { createRef, useState } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import MenuTypes from 'shared/constants/menuTypes';
import PopupMenu from './index';
export default {
component: PopupMenu,
title: 'PopupMenu',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
active: true,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
active: false,
},
];
export const LabelsPopup = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu
title="Label"
menuType={MenuTypes.LABEL_MANAGER}
top={10}
onClose={() => setPopupOpen(false)}
left={10}
onLabelEdit={action('label edit')}
onLabelToggle={action('label toggle')}
labels={labelData}
/>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
);
};
export const LabelsLabelEditor = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu
title="Change Label"
menuType={MenuTypes.LABEL_EDITOR}
top={10}
onClose={() => setPopupOpen(false)}
left={10}
onLabelEdit={action('label edit')}
onLabelToggle={action('label toggle')}
labels={labelData}
/>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
);
};

View File

@ -0,0 +1,251 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{ 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;
overflow: hidden;
position: absolute;
width: 304px;
z-index: 70;
&:focus {
outline: none;
border: none;
}
`;
export const Header = styled.div`
height: 40px;
position: relative;
margin-bottom: 8px;
text-align: center;
`;
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #5e6c84;
display: block;
line-height: 40px;
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 1;
`;
export const Content = styled.div`
max-height: 632px;
overflow-x: hidden;
overflow-y: auto;
padding: 0 12px 12px;
`;
export const LabelSearch = 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;
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;
`;
export const Section = styled.div`
margin-top: 12px;
`;
export const SectionTitle = styled.h4`
color: #5e6c84;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 16px;
margin-top: 16px;
text-transform: uppercase;
`;
export const Labels = styled.ul`
list-style: none;
margin: 0;
padding: 0;
margin-bottom: 8px;
`;
export const Label = styled.li`
padding-right: 36px;
position: relative;
`;
export const CardLabel = styled.span<{ active: boolean; color: string }>`
${props =>
props.active &&
css`
margin-left: 4px;
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
border-radius: 3px;
`}
cursor: pointer;
font-weight: 700;
margin: 0 0 4px;
min-height: 20px;
padding: 6px 12px;
position: relative;
transition: padding 85ms, margin 85ms, box-shadow 85ms;
background-color: ${props => props.color};
color: #fff;
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const CloseButton = styled.div`
padding: 10px 12px 10px 8px;
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
height: 20px;
width: 20px;
cursor: pointer;
`;
export const LabelIcon = styled.div`
border-radius: 3px;
padding: 6px;
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
font-size: 16px;
line-height: 20px;
width: 20px;
cursor: pointer;
&:hover {
background: rgba(9, 30, 66, 0.08);
}
`;
export const ActiveIcon = styled.div`
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
right: 0;
opacity: 0.85;
font-size: 16px;
line-height: 20px;
width: 20px;
`;
export const EditLabelForm = styled.form`
display: flex;
flex-direction: column;
`;
export const FieldLabel = styled.label`
font-weight: 700;
color: #5e6c84;
font-size: 12px;
line-height: 16px;
margin-top: 12px;
margin-bottom: 4px;
display: block;
`;
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;
font-size: 12px;
font-weight: 400;
`;
export const LabelBox = styled.span<{ color: string }>`
float: left;
height: 32px;
margin: 0 8px 8px 0;
padding: 0;
width: 48px;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
`;
export const SaveButton = styled.input`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
padding-left: 24px;
padding-right: 24px;
ursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
`;
export const DeleteButton = styled.input`
background-color: #cf513d;
box-shadow: none;
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
float: right;
`;

View File

@ -0,0 +1,49 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import MenuTypes from 'shared/constants/menuTypes';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from './LabelManager';
import LabelEditor from './LabelEditor';
import { Container, Header, HeaderTitle, Content, Label, CloseButton } from './Styles';
type Props = {
title: string;
top: number;
left: number;
menuType: number;
labels?: Label[];
onClose: () => void;
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
};
const PopupMenu = ({ title, menuType, labels, top, left, onClose, onLabelToggle, onLabelEdit }: Props) => {
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>
{menuType === MenuTypes.LABEL_MANAGER && (
<LabelManager onLabelEdit={onLabelEdit} onLabelToggle={onLabelToggle} labels={labels} />
)}
{menuType === MenuTypes.LABEL_EDITOR && (
<LabelEditor
onLabelEdit={onLabelEdit}
label={{ active: false, color: LabelColors.GREEN, name: 'General', labelId: 'general' }}
/>
)}
</Content>
</Container>
);
};
export default PopupMenu;

View File

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { action } from '@storybook/addon-actions';
import ProjectGridItem from './';
export default {
component: ProjectGridItem,
title: 'ProjectGridItem',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const projectsData = [
{ taskGroups: [], teamTitle: 'Personal', projectID: 'aaaa', name: 'Citadel', color: '#aa62e3' },
{ taskGroups: [], teamTitle: 'Personal', projectID: 'bbbb', name: 'Editorial Calender', color: '#aa62e3' },
{ taskGroups: [], teamTitle: 'Personal', projectID: 'cccc', name: 'New Blog', color: '#aa62e3' },
];
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
`;
const ProjectsWrapper = styled.div`
width: 60%;
display: flex;
align-items: center;
justify-content: center;
`;
export const Default = () => {
return (
<Container>
<ProjectsWrapper>
{projectsData.map(project => (
<ProjectGridItem project={project} />
))}
</ProjectsWrapper>
</Container>
);
};

View File

@ -0,0 +1,44 @@
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ProjectContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
export const ProjectTitle = styled.span`
font-size: 18px;
font-weight: 700;
transition: transform 0.25s ease;
text-align: center;
`;
export const TeamTitle = styled.span`
margin-top: 5px;
font-size: 14px;
font-weight: normal;
text-align: center;
color: #c2c6dc;
`;
export const ProjectWrapper = styled.div<{ color: string }>`
display: flex;
align-items: center;
padding: 15px 25px;
border-radius: 20px;
${mixin.boxShadowCard}
background: ${props => mixin.darken(props.color, 0.35)};
color: #fff;
cursor: pointer;
margin: 0 10px;
width: 120px;
height: 120px;
align-items: center;
justify-content: center;
transition: transform 0.25s ease;
&:hover {
transform: translateY(-5px);
}
`;

View File

@ -0,0 +1,21 @@
import React from 'react';
import { ProjectWrapper, ProjectContent, ProjectTitle, TeamTitle } from './Styles';
type Props = {
project: Project;
};
const ProjectsList = ({ project }: Props) => {
const color = project.color ?? '#c2c6dc';
return (
<ProjectWrapper color={color}>
<ProjectContent>
<ProjectTitle>{project.name}</ProjectTitle>
<TeamTitle>{project.teamTitle}</TeamTitle>
</ProjectContent>
</ProjectWrapper>
);
};
export default ProjectsList;

View File

@ -0,0 +1,96 @@
import React, { createRef, useState } from 'react';
import { action } from '@storybook/addon-actions';
import Card from 'shared/components/Card';
import CardComposer from 'shared/components/CardComposer';
import LabelColors from 'shared/constants/labelColors';
import List, { ListCards } from 'shared/components/List';
import QuickCardEditor from 'shared/components/QuickCardEditor';
export default {
component: QuickCardEditor,
title: 'QuickCardEditor',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
active: false,
},
];
export const Default = () => {
const $cardRef: any = createRef();
const [isEditorOpen, setEditorOpen] = useState(false);
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
return (
<>
{isEditorOpen && (
<QuickCardEditor
isOpen={isEditorOpen}
listId="1"
cardId="1"
cardTitle="Hello, world"
onCloseEditor={() => setEditorOpen(false)}
onEditCard={action('edit card')}
onOpenPopup={action('open popup')}
onArchiveCard={action('archive card')}
labels={labelData}
top={top}
left={left}
/>
)}
<List
id="1"
name="General"
isComposerOpen={false}
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
>
<ListCards>
<Card
cardId="1"
listId="1"
description="hello!"
ref={$cardRef}
title="Hello, world"
onClick={action('on click')}
onContextMenu={e => {
setTop(e.top);
setLeft(e.left);
setEditorOpen(true);
}}
watched
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
<CardComposer
onClose={() => {
console.log('close!');
}}
onCreateCard={name => {
console.log(name);
}}
isOpen={false}
/>
</ListCards>
</List>
</>
);
};

View File

@ -0,0 +1,144 @@
import styled, { keyframes } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.6);
bottom: 0;
color: #fff;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 30;
visibility: ${props => (props.open ? 'show' : 'hidden')};
`;
export const Container = styled.div<{ top: number; left: number }>`
position: absolute;
width: 256px;
top: ${props => props.top}px;
left: ${props => props.left}px;
`;
export const Editor = styled.div`
background-color: #fff;
border-radius: 3px;
box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);
padding: 6px 8px 2px;
cursor: default;
display: block;
margin-bottom: 8px;
max-width: 300px;
min-height: 20px;
position: relative;
text-decoration: none;
z-index: 1;
`;
export const EditorDetails = styled.div`
overflow: hidden;
padding: 0;
position: relative;
z-index: 10;
`;
export const EditorTextarea = styled(TextareaAutosize)`
font-family: 'Droid Sans';
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
background: none;
border: none;
box-shadow: none;
margin-bottom: 4px;
max-height: 162px;
min-height: 54px;
padding: 0;
font-size: 16px;
line-height: 20px;
&:focus {
border: none;
outline: none;
}
`;
export const SaveButton = styled.button`
cursor: pointer;
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
font-weight: 400;
line-height: 20px;
margin-top: 8px;
margin-right: 4px;
padding: 6px 24px;
text-align: center;
border-radius: 3px;
`;
export const FadeInAnimation = keyframes`
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
`;
export const EditorButtons = styled.div`
left: 100%;
position: absolute;
top: 0;
width: 240px;
z-index: 0;
animation: ${FadeInAnimation} 85ms ease-in 1;
`;
export const EditorButton = styled.div`
cursor: pointer;
background: rgba(0, 0, 0, 0.6);
border-radius: 3px;
clear: both;
color: #e6e6e6;
display: block;
float: left;
margin: 0 0 4px 8px;
padding: 6px 12px 6px 8px;
text-decoration: none;
transition: transform 85ms ease-in;
`;
export const CloseButton = styled.div`
padding: 9px;
position: absolute;
right: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
opacity: 0.8;
z-index: 40;
cursor: pointer;
`;
export const ListCardLabels = styled.div`
overflow: auto;
position: relative;
`;
export const ListCardLabel = styled.span`
height: 16px;
line-height: 16px;
padding: 0 8px;
max-width: 198px;
float: left;
font-size: 12px;
font-weight: 700;
margin: 0 4px 4px 0;
width: auto;
border-radius: 4px;
color: #fff;
display: block;
position: relative;
background-color: ${props => props.color};
`;

View File

@ -0,0 +1,119 @@
import React, { useRef, useState, useEffect } from 'react';
import Cross from 'shared/icons/Cross';
import {
Wrapper,
Container,
Editor,
EditorDetails,
EditorTextarea,
SaveButton,
EditorButtons,
EditorButton,
CloseButton,
ListCardLabels,
ListCardLabel,
} from './Styles';
type Props = {
listId: string;
cardId: string;
cardTitle: string;
onCloseEditor: () => void;
onEditCard: (listId: string, cardId: string, cardName: string) => void;
onOpenPopup: (popupType: number, top: number, left: number) => void;
onArchiveCard: (listId: string, cardId: string) => void;
labels?: Label[];
isOpen: boolean;
top: number;
left: number;
};
const QuickCardEditor = ({
listId,
cardId,
cardTitle,
onCloseEditor,
onOpenPopup,
onArchiveCard,
onEditCard,
labels,
isOpen,
top,
left,
}: Props) => {
const [currentCardTitle, setCardTitle] = useState(cardTitle);
const $editorRef: any = useRef();
const $labelsRef: any = useRef();
useEffect(() => {
$editorRef.current.focus();
$editorRef.current.select();
}, []);
const handleCloseEditor = (e: any) => {
e.stopPropagation();
onCloseEditor();
};
const handleKeyDown = (e: any) => {
if (e.key === 'Enter') {
e.preventDefault();
onEditCard(listId, cardId, currentCardTitle);
onCloseEditor();
}
};
return (
<Wrapper onClick={handleCloseEditor} open={isOpen}>
<CloseButton onClick={handleCloseEditor}>
<Cross size={16} color="#000" />
</CloseButton>
<Container left={left} top={top}>
<Editor>
<ListCardLabels>
{labels &&
labels.map(label => (
<ListCardLabel color={label.color} key={label.name}>
{label.name}
</ListCardLabel>
))}
</ListCardLabels>
<EditorDetails>
<EditorTextarea
onChange={e => setCardTitle(e.currentTarget.value)}
onClick={e => {
e.stopPropagation();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
/>
</EditorDetails>
</Editor>
<SaveButton onClick={e => onEditCard(listId, cardId, currentCardTitle)}>Save</SaveButton>
<EditorButtons>
<EditorButton
ref={$labelsRef}
onClick={e => {
e.stopPropagation();
const pos = $labelsRef.current.getBoundingClientRect();
onOpenPopup(1, pos.top + $labelsRef.current.clientHeight + 4, pos.left);
}}
>
Edit Labels
</EditorButton>
<EditorButton
onClick={e => {
e.stopPropagation();
onArchiveCard(listId, cardId);
onCloseEditor();
}}
>
Archive
</EditorButton>
</EditorButtons>
</Container>
</Wrapper>
);
};
export default QuickCardEditor;

View File

@ -0,0 +1,29 @@
import React from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Sidebar from './index';
import Navbar from 'shared/components/Navbar';
export default {
component: Sidebar,
title: 'Sidebar',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Navbar />
<Sidebar />
</>
);
};

View File

@ -0,0 +1,15 @@
import styled from 'styled-components';
export const Container = styled.div`
position: fixed;
z-index: 99;
top: 0px;
left: 80px;
height: 100vh;
width: 230px;
overflow-x: hidden;
overflow-y: auto;
padding: 0px 16px 24px;
background: rgb(244, 245, 247);
border-right: 1px solid rgb(223, 225, 230);
`;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { Container } from './Styles';
const Sidebar = () => {
return <Container></Container>;
};
export default Sidebar;

View File

@ -0,0 +1,70 @@
import styled from 'styled-components';
export const NavbarWrapper = styled.div`
height: 103px;
padding: 1.3rem 2.2rem 2.2rem;
width: 100%;
`;
export const NavbarHeader = styled.header`
border-radius: 0.5rem;
padding: 0.8rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
background: rgb(16, 22, 58);
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
`;
export const Breadcrumbs = styled.div`
color: rgb(94, 108, 132);
font-size: 15px;
`;
export const BreadcrumpSeparator = styled.span`
position: relative;
top: 2px;
font-size: 18px;
margin: 0px 10px;
`;
export const ProjectActions = styled.div``;
export const GlobalActions = styled.div`
display: flex;
align-items: center;
`;
export const ProfileContainer = styled.div`
display: flex;
align-items: center;
`;
export const ProfileNameWrapper = styled.div`
text-align: right;
line-height: 1.25;
`;
export const NotificationContainer = styled.div`
margin-right: 20px;
cursor: pointer;
`;
export const ProfileNamePrimary = styled.div`
color: #c2c6dc;
font-weight: 600;
`;
export const ProfileNameSecondary = styled.small`
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
margin-left: 10px;
width: 40px;
height: 40px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
`;

View File

@ -0,0 +1,46 @@
import React, { createRef, useState } from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import TopNavbar from './index';
import { action } from '@storybook/addon-actions';
import DropdownMenu from 'shared/components/DropdownMenu';
export default {
component: TopNavbar,
title: 'TopNavbar',
// Our exports that end in "Data" are not stories.
excludeStories: /.*Data$/,
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#f8f8f8' },
{ name: 'darkBlue', value: '#262c49', default: true },
],
},
};
export const Default = () => {
const [menu, setMenu] = useState({
top: 0,
left: 0,
isOpen: false,
});
const onClick = (bottom: number, right: number) => {
setMenu({
isOpen: !menu.isOpen,
left: right,
top: bottom,
});
};
return (
<>
<NormalizeStyles />
<BaseStyles />
<TopNavbar onNotificationClick={action('notifications click')} onProfileClick={onClick} />
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
</>
);
};

View File

@ -0,0 +1,61 @@
import React, { useRef } from 'react';
import { Bell } from 'shared/icons';
import {
NotificationContainer,
GlobalActions,
ProjectActions,
NavbarWrapper,
NavbarHeader,
Breadcrumbs,
BreadcrumpSeparator,
ProfileIcon,
ProfileContainer,
ProfileNameWrapper,
ProfileNamePrimary,
ProfileNameSecondary,
} from './Styles';
type NavBarProps = {
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
};
const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick }) => {
const $profileRef: any = useRef(null);
const handleProfileClick = () => {
console.log('click');
const boundingRect = $profileRef.current.getBoundingClientRect();
onProfileClick(boundingRect.bottom, boundingRect.right);
};
return (
<NavbarWrapper>
<NavbarHeader>
<ProjectActions>
<Breadcrumbs>
Projects
<BreadcrumpSeparator>/</BreadcrumpSeparator>
project name
<BreadcrumpSeparator>/</BreadcrumpSeparator>
Board
</Breadcrumbs>
</ProjectActions>
<GlobalActions>
<NotificationContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
</NotificationContainer>
<ProfileContainer>
<ProfileNameWrapper>
<ProfileNamePrimary>Jordan Knott</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick}>
JK
</ProfileIcon>
</ProfileContainer>
</GlobalActions>
</NavbarHeader>
</NavbarWrapper>
);
};
export default NavBar;