arch: move web folder into api & move api to top level

This commit is contained in:
Jordan Knott
2020-07-04 18:08:37 -05:00
parent eaffaa70df
commit e5d5e6da01
354 changed files with 20 additions and 1557 deletions

View File

@ -0,0 +1,19 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import AddList from '.';
export default {
component: AddList,
title: 'AddList',
parameters: {
backgrounds: [
{ name: 'gray', value: '#262c49', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return <AddList onSave={action('on save')} />;
};

View File

@ -0,0 +1,113 @@
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
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 Wrapper = styled.div<{ editorOpen: boolean }>`
display: inline-block;
background-color: hsla(0, 0%, 100%, 0.24);
cursor: pointer;
border-radius: 3px;
height: auto;
min-height: 32px;
padding: 4px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
width: 272px;
margin: 0 4px;
margin-right: 8px;
${props =>
!props.editorOpen &&
css`
&:hover {
background-color: hsla(0, 0%, 100%, 0.32);
}
`}
${props =>
props.editorOpen &&
css`
background-color: #10163a;
border-radius: 3px;
height: auto;
min-height: 32px;
padding: 8px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
`}
`;
export const AddListButton = styled(Button)`
padding: 6px 12px;
`;
export const Placeholder = styled.span`
color: #c2c6dc;
display: flex;
align-items: center;
padding: 6px 8px;
transition: color 85ms ease-in;
`;
export const AddIconWrapper = styled.div`
color: #fff;
margin-right: 6px;
`;
export const ListNameEditorWrapper = styled.div`
display: flex;
`;
export const ListNameEditor = styled(TextareaAutosize)`
background-color: ${props => mixin.lighten('#262c49', 0.05)};
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
transition: margin 85ms ease-in, background 85ms ease-in;
line-height: 20px;
padding: 8px 12px;
font-family: 'Droid Sans';
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
border: none;
border-radius: 3px;
box-shadow: none;
margin-bottom: 4px;
max-height: 162px;
min-height: 54px;
font-size: 14px;
line-height: 20px;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`;
export const ListAddControls = styled.div`
height: 32px;
transition: margin 85ms ease-in, height 85ms ease-in;
overflow: hidden;
margin: 4px 0 0;
`;
export const CancelAdd = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;

View File

@ -0,0 +1,111 @@
import React, { useState, useRef, useEffect } from 'react';
import { Plus, Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import Button from 'shared/components/Button';
import {
Container,
Wrapper,
Placeholder,
AddIconWrapper,
AddListButton,
ListNameEditor,
ListAddControls,
CancelAdd,
ListNameEditorWrapper,
} from './Styles';
type NameEditorProps = {
onSave: (listName: string) => void;
onCancel: () => void;
};
const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
const $editorRef = useRef<HTMLTextAreaElement>(null);
const [listName, setListName] = useState('');
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
});
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
onSave(listName);
setListName('');
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
}
};
return (
<>
<ListNameEditorWrapper>
<ListNameEditor
ref={$editorRef}
onKeyDown={onKeyDown}
value={listName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
placeholder="Enter a title for this list..."
/>
</ListNameEditorWrapper>
<ListAddControls>
<AddListButton
variant="relief"
onClick={() => {
onSave(listName);
setListName('');
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
}}
>
Save
</AddListButton>
<CancelAdd onClick={() => onCancel()}>
<Cross width={16} height={16} />
</CancelAdd>
</ListAddControls>
</>
);
};
type AddListProps = {
onSave: (listName: string) => void;
};
const AddList: React.FC<AddListProps> = ({ onSave }) => {
const [editorOpen, setEditorOpen] = useState(false);
const $wrapperRef = useRef<HTMLDivElement>(null);
const onOutsideClick = () => {
setEditorOpen(false);
};
useOnOutsideClick($wrapperRef, editorOpen, onOutsideClick, null);
return (
<Container>
<Wrapper
ref={$wrapperRef}
editorOpen={editorOpen}
onClick={() => {
if (!editorOpen) {
setEditorOpen(true);
}
}}
>
{editorOpen ? (
<NameEditor onCancel={() => setEditorOpen(false)} onSave={onSave} />
) : (
<Placeholder>
<AddIconWrapper>
<Plus size={12} color="#c2c6dc" />
</AddIconWrapper>
Add another list
</Placeholder>
)}
</Wrapper>
</Container>
);
};
export default AddList;

View File

@ -0,0 +1,49 @@
import React, { useRef } from 'react';
import Admin from '.';
import { theme } from 'App/ThemeStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import { ThemeProvider } from 'styled-components';
import { action } from '@storybook/addon-actions';
export default {
component: Admin,
title: 'Admin',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<ThemeProvider theme={theme}>
<Admin
onInviteUser={action('invite user')}
initialTab={1}
onDeleteUser={action('delete user')}
users={[
{
id: '1',
username: 'jordanthedev',
email: 'jordan@jordanthedev.com',
role: { code: 'admin', name: 'Admin' },
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#fff',
initials: 'JK',
url: null,
},
},
]}
onAddUser={action('add user')}
/>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,373 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-material.css';
import Button from 'shared/components/Button';
const NewUserButton = styled(Button)`
padding: 6px 12px;
margin-right: 12px;
`;
const InviteUserButton = styled(Button)`
padding: 6px 12px;
margin-right: 8px;
`;
const MemberActions = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
`;
const GridTable = styled.div`
height: 620px;
`;
const RootWrapper = styled.div`
height: 100%;
display: flex;
position: relative;
flex-direction: column;
overflow: hidden;
`;
const Root = styled.div`
.ag-theme-material {
--ag-foreground-color: #c2c6dc;
--ag-secondary-foreground-color: #c2c6dc;
--ag-background-color: transparent;
--ag-header-background-color: transparent;
--ag-header-foreground-color: #c2c6dc;
--ag-border-color: #414561;
--ag-row-hover-color: #262c49;
--ag-header-cell-hover-background-color: #262c49;
--ag-checkbox-unchecked-color: #c2c6dc;
--ag-checkbox-indeterminate-color: rgba(115, 103, 240);
--ag-selected-row-background-color: #262c49;
--ag-material-primary-color: rgba(115, 103, 240);
--ag-material-accent-color: rgba(115, 103, 240);
}
.ag-theme-material ::-webkit-scrollbar {
width: 12px;
}
.ag-theme-material ::-webkit-scrollbar-track {
background: #262c49;
border-radius: 20px;
}
.ag-theme-material ::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 20px;
}
.ag-header-cell-text {
color: #fff;
font-weight: 700;
}
`;
const Header = styled.div`
border-bottom: 1px solid #e2e2e2;
flex-direction: row;
box-sizing: border-box;
display: flex;
white-space: nowrap;
width: 100%;
overflow: hidden;
background: transparent;
border-bottom-color: #414561;
color: #fff;
height: 112px;
min-height: 112px;
`;
const EditUserIcon = styled(Pencil)``;
const LockUserIcon = styled(Lock)``;
const DeleteUserIcon = styled(Trash)``;
type ActionButtonProps = {
onClick: ($target: React.RefObject<HTMLElement>) => void;
};
const ActionButtonWrapper = styled.div`
margin-right: 8px;
cursor: pointer;
display: inline-flex;
`;
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
const $wrapper = useRef<HTMLDivElement>(null);
return (
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
{children}
</ActionButtonWrapper>
);
};
const ActionButtons = (params: any) => {
return (
<>
<ActionButton onClick={() => {}}>
<EditUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={() => {}}>
<LockUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} />
</ActionButton>
</>
);
};
type ListTableProps = {
users: Array<User>;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
};
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
{
minWidth: 200,
headerName: 'Actions',
field: 'id',
cellRenderer: 'actionButtons',
cellRendererParams: {
onDeleteUser: (target: any, userID: any) => {
onDeleteUser(target, userID);
},
},
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
};
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={params => {
params.api.sizeColumnsToFit();
}}
/>
</div>
</Root>
);
};
const Wrapper = styled.div`
background: #eff2f7;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
position: relative;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
height: 48px;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover {
color: rgba(115, 103, 240);
}
&:hover svg {
fill: rgba(115, 103, 240);
}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
position: relative;
display: block;
overflow: hidden;
width: 100%;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
margin-left: 1rem !important;
border-radius: 0.5rem;
`;
const items = [
{ name: 'Insights' },
{ name: 'Members' },
{ name: 'Teams' },
{ name: 'Security' },
{ name: 'Settings' },
];
type NavItemProps = {
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
};
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
};
type AdminProps = {
initialTab: number;
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>;
};
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onInviteUser, users }) => {
const [currentTop, setTop] = useState(initialTab * 48);
const [currentTab, setTab] = useState(initialTab);
const $tabNav = useRef<HTMLDivElement>(null);
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<MemberActions>
<NewUserButton variant="outline" onClick={onAddUser}>
<Plus color="rgba(115, 103, 240)" size={10} />
<span style={{ paddingLeft: '5px' }}>Create member</span>
</NewUserButton>
<InviteUserButton variant="outline" onClick={onInviteUser}>
<Plus color="rgba(115, 103, 240)" size={10} />
<span style={{ paddingLeft: '5px' }}>Invite member</span>
</InviteUserButton>
</MemberActions>
<ListTable onDeleteUser={onDeleteUser} users={users} />
</TabContent>
</TabContentWrapper>
</Container>
);
};
export default Admin;

View File

@ -0,0 +1,138 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import styled, { ThemeProvider } from 'styled-components';
import Button from '.';
export default {
component: Button,
title: 'Button',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
const ButtonRow = styled.div`
display: flex;
align-items: center;
justify-items: center;
margin: 25px;
width: 100%;
& > button {
margin-right: 1.5rem;
}
`;
export const Default = () => {
return (
<>
<BaseStyles />
<NormalizeStyles />
<ThemeProvider theme={theme}>
<ButtonRow>
<Button>Primary</Button>
<Button color="success">Success</Button>
<Button color="danger">Danger</Button>
<Button color="warning">Warning</Button>
<Button color="dark">Dark</Button>
<Button disabled>Disabled</Button>
</ButtonRow>
<ButtonRow>
<Button variant="outline">Primary</Button>
<Button variant="outline" color="success">
Success
</Button>
<Button variant="outline" color="danger">
Danger
</Button>
<Button variant="outline" color="warning">
Warning
</Button>
<Button variant="outline" color="dark">
Dark
</Button>
<Button variant="outline" disabled>
Disabled
</Button>
</ButtonRow>
<ButtonRow>
<Button variant="flat">Primary</Button>
<Button variant="flat" color="success">
Success
</Button>
<Button variant="flat" color="danger">
Danger
</Button>
<Button variant="flat" color="warning">
Warning
</Button>
<Button variant="flat" color="dark">
Dark
</Button>
<Button variant="flat" disabled>
Disabled
</Button>
</ButtonRow>
<ButtonRow>
<Button variant="lineDown">Primary</Button>
<Button variant="lineDown" color="success">
Success
</Button>
<Button variant="lineDown" color="danger">
Danger
</Button>
<Button variant="lineDown" color="warning">
Warning
</Button>
<Button variant="lineDown" color="dark">
Dark
</Button>
<Button variant="lineDown" disabled>
Disabled
</Button>
</ButtonRow>
<ButtonRow>
<Button variant="gradient">Primary</Button>
<Button variant="gradient" color="success">
Success
</Button>
<Button variant="gradient" color="danger">
Danger
</Button>
<Button variant="gradient" color="warning">
Warning
</Button>
<Button variant="gradient" color="dark">
Dark
</Button>
<Button variant="gradient" disabled>
Disabled
</Button>
</ButtonRow>
<ButtonRow>
<Button variant="relief">Primary</Button>
<Button variant="relief" color="success">
Success
</Button>
<Button variant="relief" color="danger">
Danger
</Button>
<Button variant="relief" color="warning">
Warning
</Button>
<Button variant="relief" color="dark">
Dark
</Button>
<Button variant="relief" disabled>
Disabled
</Button>
</ButtonRow>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,197 @@
import React, { useRef } from 'react';
import styled, { css } from 'styled-components/macro';
const Text = styled.span<{ fontSize: string }>`
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: ${props => props.fontSize};
color: rgba(${props => props.theme.colors.text.secondary});
`;
const Base = styled.button<{ color: string; disabled: boolean }>`
transition: all 0.2s ease;
position: relative;
border: none;
cursor: pointer;
padding: 0.75rem 2rem;
border-radius: ${props => props.theme.borderRadius.alternate};
${props =>
props.disabled &&
css`
opacity: 0.5;
cursor: default;
pointer-events: none;
`}
`;
const Filled = styled(Base)`
background: rgba(${props => props.theme.colors[props.color]});
&:hover {
box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]});
}
`;
const Outline = styled(Base)`
border: 1px solid rgba(${props => props.theme.colors[props.color]});
background: transparent;
& ${Text} {
color: rgba(${props => props.theme.colors[props.color]});
}
&:hover {
background: rgba(${props => props.theme.colors[props.color]}, 0.08);
}
`;
const Flat = styled(Base)`
background: transparent;
&:hover {
background: rgba(${props => props.theme.colors[props.color]}, 0.2);
}
`;
const LineX = styled.span<{ color: string }>`
transition: all 0.2s ease;
position: absolute;
height: 2px;
width: 0;
top: auto;
bottom: -2px;
left: 50%;
transform: translate(-50%);
background: rgba(${props => props.theme.colors[props.color]}, 1);
`;
const LineDown = styled(Base)`
background: transparent;
border-radius: 0;
border-width: 0;
border-style: solid;
border-bottom-width: 2px;
border-color: rgba(${props => props.theme.colors[props.color]}, 0.2);
&:hover ${LineX} {
width: 100%;
}
&:hover ${Text} {
transform: translateY(2px);
}
`;
const Gradient = styled(Base)`
background: linear-gradient(
30deg,
rgba(${props => props.theme.colors[props.color]}, 1),
rgba(${props => props.theme.colors[props.color]}, 0.5)
);
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
&:hover {
transform: translateY(-2px);
}
`;
const Relief = styled(Base)`
background: rgba(${props => props.theme.colors[props.color]}, 1);
-webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
&:active {
transform: translateY(3px);
box-shadow: none !important;
}
`;
type ButtonProps = {
fontSize?: string;
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
disabled?: boolean;
type?: 'button' | 'submit';
className?: string;
onClick?: ($target: React.RefObject<HTMLButtonElement>) => void;
};
const Button: React.FC<ButtonProps> = ({
disabled = false,
fontSize = '14px',
color = 'primary',
variant = 'filled',
type = 'button',
onClick,
className,
children,
}) => {
const $button = useRef<HTMLButtonElement>(null);
const handleClick = () => {
if (onClick) {
onClick($button);
}
};
switch (variant) {
case 'filled':
return (
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Filled>
);
case 'outline':
return (
<Outline
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text>
</Outline>
);
case 'flat':
return (
<Flat ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Flat>
);
case 'lineDown':
return (
<LineDown
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text>
<LineX color={color} />
</LineDown>
);
case 'gradient':
return (
<Gradient
ref={$button}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
<Text fontSize={fontSize}>{children}</Text>
</Gradient>
);
case 'relief':
return (
<Relief ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Relief>
);
default:
throw new Error('not a valid variant');
}
};
export default Button;

View File

@ -0,0 +1,174 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import Card from '.';
export default {
component: Card,
title: 'Card',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
const labelData: Array<ProjectLabel> = [
{
id: 'development',
name: 'Development',
createdDate: new Date().toString(),
labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
},
];
export const Default = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
taskID="1"
taskGroupID="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
taskID="1"
taskGroupID="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
taskID="1"
taskGroupID="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
taskID="1"
taskGroupID="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
taskID="1"
taskGroupID="1"
description="hello!"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
watched
members={[
{
id: '1',
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#0079bf',
initials: 'JK',
url: null,
},
},
]}
labels={labelData}
checklists={{ complete: 1, total: 4 }}
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
/>
);
};
export const Members = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
description={null}
taskID="1"
taskGroupID="1"
ref={$ref}
title="Hello, world"
onClick={action('on click')}
onContextMenu={action('on context click')}
members={[
{
id: '1',
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#0079bf',
initials: 'JK',
url: null,
},
},
]}
labels={[]}
/>
);
};
export const Editable = () => {
const $ref = useRef<HTMLDivElement>(null);
return (
<Card
taskID="1"
taskGroupID="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' }}
editable
onEditCard={action('edit card')}
/>
);
};

View File

@ -0,0 +1,171 @@
import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle } from 'shared/icons';
import { RefObject } from 'react';
export const ClockIcon = styled(FontAwesomeIcon)``;
export const EditorTextarea = styled(TextareaAutosize)`
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: 16px;
color: rgba(${props => props.theme.colors.text.primary});
&:focus {
border: none;
outline: none;
}
`;
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;
font-size: 12px;
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 }>`
font-size: 12px;
${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; editable: boolean }>`
max-width: 256px;
margin-bottom: 8px;
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer !important;
position: relative;
background-color: ${props =>
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`};
`;
export const ListCardInnerContainer = styled.div`
width: 100%;
height: 100%;
`;
export const ListCardDetails = styled.div<{ complete: boolean }>`
overflow: hidden;
padding: 6px 8px 2px;
position: relative;
z-index: 10;
${props => props.complete && 'opacity: 0.6;'}
`;
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;
border-radius: 3px;
opacity: 0.8;
padding: 6px;
position: absolute;
right: 2px;
top: 2px;
z-index: 10;
&:hover {
background-color: ${props => mixin.darken('#262c49', 0.45)};
}
`;
export const CardTitle = styled.span`
clear: both;
margin: 0 0 4px;
overflow: hidden;
text-decoration: none;
word-wrap: break-word;
line-height: 16px;
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
display: flex;
align-items: center;
`;
export const CardMembers = styled.div`
float: right;
margin: 0 -2px 4px 0;
`;
export const CompleteIcon = styled(CheckCircle)`
fill: rgba(${props => props.theme.colors.success});
margin-right: 4px;
flex-shrink: 0;
`;
export const EditorContent = styled.div`
display: flex;
`;

View File

@ -0,0 +1,220 @@
import React, { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import TaskAssignee from 'shared/components/TaskAssignee';
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
import { faClock, faCheckSquare, faEye } from '@fortawesome/free-regular-svg-icons';
import {
EditorTextarea,
EditorContent,
CompleteIcon,
DescriptionBadge,
DueDateCardBadge,
ListCardBadges,
ListCardBadge,
ListCardBadgeText,
ListCardContainer,
ListCardInnerContainer,
ListCardDetails,
ClockIcon,
ListCardLabels,
ListCardLabel,
ListCardOperation,
CardTitle,
CardMembers,
} from './Styles';
type DueDate = {
isPastDue: boolean;
formattedDate: string;
};
type Checklist = {
complete: number;
total: number;
};
type Props = {
title: string;
taskID: string;
taskGroupID: string;
complete?: boolean;
onContextMenu?: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
description?: null | string;
dueDate?: DueDate;
checklists?: Checklist | null;
labels?: Array<ProjectLabel>;
watched?: boolean;
wrapperProps?: any;
members?: Array<TaskUser> | null;
onCardMemberClick?: OnCardMemberClick;
editable?: boolean;
onEditCard?: (taskGroupID: string, taskID: string, cardName: string) => void;
onCardTitleChange?: (name: string) => void;
};
const Card = React.forwardRef(
(
{
wrapperProps,
onContextMenu,
taskID,
taskGroupID,
complete,
onClick,
labels,
title,
dueDate,
description,
checklists,
watched,
members,
onCardMemberClick,
editable,
onEditCard,
onCardTitleChange,
}: Props,
$cardRef: any,
) => {
const [currentCardTitle, setCardTitle] = useState(title);
const $editorRef: any = useRef();
useEffect(() => {
setCardTitle(title);
}, [title]);
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
$editorRef.current.select();
}
}, []);
const handleKeyDown = (e: any) => {
if (e.key === 'Enter') {
e.preventDefault();
if (onEditCard) {
onEditCard(taskGroupID, taskID, currentCardTitle);
}
}
};
const [isActive, setActive] = useState(false);
const $innerCardRef: any = useRef(null);
const onOpenComposer = () => {
if (onContextMenu) {
onContextMenu($innerCardRef, taskID, taskGroupID);
}
};
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={e => {
if (onClick) {
onClick(e);
}
}}
onContextMenu={onTaskContext}
isActive={isActive}
editable={editable}
{...wrapperProps}
>
<ListCardInnerContainer ref={$innerCardRef}>
{isActive && (
<ListCardOperation>
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
</ListCardOperation>
)}
<ListCardDetails complete={complete ?? false}>
<ListCardLabels>
{labels &&
labels.map(label => (
<ListCardLabel color={label.labelColor.colorHex} key={label.id}>
{label.name}
</ListCardLabel>
))}
</ListCardLabels>
{editable ? (
<EditorContent>
{complete && <CompleteIcon width={16} height={16} />}
<EditorTextarea
onChange={e => {
setCardTitle(e.currentTarget.value);
if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value);
}
}}
onClick={e => {
e.stopPropagation();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
/>
</EditorContent>
) : (
<CardTitle>
{complete && <CompleteIcon width={16} height={16} />}
{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>
<CardMembers>
{members &&
members.map(member => (
<TaskAssignee
key={member.id}
size={28}
member={member}
onMemberProfile={$target => {
if (onCardMemberClick) {
onCardMemberClick($target, taskID, member.id);
}
}}
/>
))}
</CardMembers>
</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 '.';
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,35 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
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 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)`
margin-right: 4px;
padding: 6px 12px;
border-radius: 3px;
`;

View File

@ -0,0 +1,72 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import {
CardComposerWrapper,
CancelIcon,
AddCardButton,
ComposerControls,
ComposerControlsSaveSection,
ComposerControlsActionsSection,
} from './Styles';
import Card from '../Card';
type Props = {
isOpen: boolean;
onCreateCard: (cardName: string) => void;
onClose: () => void;
};
const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
const [cardName, setCardName] = useState('');
const $cardRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($cardRef, true, onClose, null);
useOnEscapeKeyDown(isOpen, onClose);
return (
<CardComposerWrapper isOpen={isOpen}>
<Card
title={cardName}
ref={$cardRef}
taskID=""
taskGroupID=""
editable
onEditCard={(_taskGroupID, _taskID, name) => {
onCreateCard(name);
setCardName('');
}}
onCardTitleChange={name => {
setCardName(name);
}}
/>
<ComposerControls>
<ComposerControlsSaveSection>
<AddCardButton
variant="relief"
onClick={() => {
onCreateCard(cardName);
setCardName('');
}}
>
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,138 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import produce from 'immer';
import styled, { ThemeProvider } from 'styled-components';
import Checklist from '.';
export default {
component: Checklist,
title: 'Checklist',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
const Container = styled.div`
width: 552px;
margin: 25px;
border: 1px solid rgba(${props => props.theme.colors.bg.primary});
`;
const defaultItems = [
{
id: '1',
position: 1,
taskChecklistID: '1',
complete: false,
name: 'Tasks',
assigned: null,
dueDate: null,
},
{
id: '2',
taskChecklistID: '1',
position: 2,
complete: false,
name: 'Projects',
assigned: null,
dueDate: null,
},
{
id: '3',
position: 3,
taskChecklistID: '1',
complete: false,
name: 'Teams',
assigned: null,
dueDate: null,
},
{
id: '4',
position: 4,
complete: false,
taskChecklistID: '1',
name: 'Organizations',
assigned: null,
dueDate: null,
},
];
export const Default = () => {
const [checklistName, setChecklistName] = useState('Checklist');
const [items, setItems] = useState(defaultItems);
const onToggleItem = (itemID: string, complete: boolean) => {
setItems(
produce(items, draftState => {
const idx = items.findIndex(item => item.id === itemID);
if (idx !== -1) {
draftState[idx] = {
...draftState[idx],
complete,
};
}
}),
);
};
return (
<>
<BaseStyles />
<NormalizeStyles />
<ThemeProvider theme={theme}>
<Container>
<Checklist
name={checklistName}
checklistID="checklist-one"
items={items}
onDeleteChecklist={action('delete checklist')}
onChangeName={currentName => {
setChecklistName(currentName);
}}
onAddItem={itemName => {
let position = 1;
const lastItem = items[-1];
if (lastItem) {
position = lastItem.position * 2 + 1;
}
setItems([
...items,
{
id: `${Math.random()}`,
name: itemName,
complete: false,
assigned: null,
dueDate: null,
position,
taskChecklistID: '1',
},
]);
}}
onDeleteItem={itemID => {
console.log(`itemID ${itemID}`);
setItems(items.filter(item => item.id !== itemID));
}}
onChangeItemName={(itemID, currentName) => {
setItems(
produce(items, draftState => {
const idx = items.findIndex(item => item.id === itemID);
if (idx !== -1) {
draftState[idx] = {
...draftState[idx],
name: currentName,
};
}
}),
);
}}
onToggleItem={onToggleItem}
/>
</Container>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,596 @@
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus } from 'shared/icons';
import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
const Wrapper = styled.div`
margin-bottom: 24px;
`;
const WindowTitle = styled.div`
padding: 8px 0;
position: relative;
margin: 0 0 4px 40px;
`;
const WindowTitleIcon = styled(CheckSquareOutline)`
top: 10px;
left: -40px;
position: absolute;
`;
const WindowChecklistTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
flex-flow: row wrap;
`;
const WindowTitleText = styled.h3`
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary});
margin: 6px 0;
display: inline-block;
width: auto;
min-height: 18px;
font-size: 16px;
line-height: 20px;
min-width: 40px;
`;
const WindowOptions = styled.div`
margin: 0 2px 0 auto;
float: right;
`;
const DeleteButton = styled(Button)`
padding: 6px 12px;
`;
const ChecklistProgress = styled.div`
margin-bottom: 6px;
position: relative;
`;
const ChecklistProgressPercent = styled.span`
color: #5e6c84;
font-size: 11px;
line-height: 10px;
position: absolute;
left: 5px;
top: -1px;
text-align: center;
width: 32px;
`;
const ChecklistProgressBar = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
border-radius: 4px;
clear: both;
height: 8px;
margin: 0 0 0 40px;
overflow: hidden;
position: relative;
`;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
bottom: 0;
left: 0;
position: absolute;
top: 0;
transition: width 0.14s ease-in, background 0.14s ease-in;
`;
const ChecklistItems = styled.div`
min-height: 8px;
`;
const ChecklistItemUncheckedIcon = styled(Square)``;
const ChecklistIcon = styled.div`
cursor: pointer;
position: absolute;
left: 0;
top: 0;
margin: 10px;
text-align: center;
&:hover {
opacity: 0.8;
}
`;
const ChecklistItemCheckedIcon = styled(CheckSquare)`
fill: rgba(${props => props.theme.colors.primary});
`;
const ChecklistItemDetails = styled.div`
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
`;
const ChecklistItemRow = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
`;
const ChecklistItemTextControls = styled.div`
padding: 6px 0;
width: 100%;
display: inline-flex;
`;
const ChecklistItemText = styled.span<{ complete: boolean }>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px;
font-size: 16px;
min-height: 20px;
margin-bottom: 0;
align-self: center;
flex: 1;
`;
const ChecklistControls = styled.div`
display: inline-flex;
flex-direction: row;
float: right;
`;
const ControlButton = styled.div`
opacity: 0;
margin-left: 4px;
padding: 4px 6px;
border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
&:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1);
}
`;
const ChecklistNameEditorWrapper = styled.div`
display: block;
float: left;
padding-top: 6px;
padding-bottom: 8px;
z-index: 50;
width: 100%;
`;
export const ChecklistNameEditor = styled(TextareaAutosize)`
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
background: none;
border: none;
box-shadow: none;
max-height: 162px;
min-height: 54px;
padding: 8px 12px;
font-size: 16px;
line-height: 20px;
border: 1px solid rgba(${props => props.theme.colors.primary});
border-radius: 3px;
color: rgba(${props => props.theme.colors.text.primary});
border-color: rgba(${props => props.theme.colors.border});
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
&:focus {
border-color: rgba(${props => props.theme.colors.primary});
}
`;
const AssignUserButton = styled(AccountPlus)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ClockButton = styled(Clock)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ChecklistItemWrapper = styled.div`
user-select: none;
clear: both;
padding-left: 40px;
position: relative;
border-radius: 6px;
transform-origin: left bottom;
transition-property: transform, opacity, height, padding, margin;
transition-duration: 0.14s;
transition-timing-function: ease-in;
&:hover {
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
}
&:hover ${ControlButton} {
opacity: 1;
}
`;
const EditControls = styled.div`
clear: both;
display: flex;
padding-bottom: 9px;
flex-direction: row;
`;
const SaveButton = styled(Button)`
margin-right: 4px;
padding: 6px 12px;
`;
const CancelButton = styled.div`
cursor: pointer;
margin: 5px;
& svg {
fill: rgba(${props => props.theme.colors.text.primary});
}
&:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary});
}
`;
const Spacer = styled.div`
flex: 1;
`;
const EditableDeleteButton = styled.button`
cursor: pointer;
display: flex;
margin: 0 2px;
padding: 6px 8px;
border-radius: 3px;
&:hover {
background: rgba(${props => props.theme.colors.primary}, 0.8);
}
`;
const NewItemButton = styled(Button)`
padding: 6px 8px;
`;
const ChecklistNewItem = styled.div`
margin: 8px 0;
margin-left: 40px;
`;
type ChecklistItemProps = {
itemID: string;
complete: boolean;
name: string;
onChangeName: (itemID: string, currentName: string) => void;
onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (itemID: string) => void;
};
const ChecklistItem: React.FC<ChecklistItemProps> = ({
itemID,
complete,
name,
onChangeName,
onToggleItem,
onDeleteItem,
}) => {
const $item = useRef<HTMLDivElement>(null);
const $editor = useRef<HTMLTextAreaElement>(null);
const [editting, setEditting] = useState(false);
const [currentName, setCurrentName] = useState(name);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($item, true, () => setEditting(false), null);
return (
<ChecklistItemWrapper ref={$item}>
<ChecklistIcon
onClick={e => {
e.stopPropagation();
onToggleItem(itemID, !complete);
}}
>
{complete ? (
<ChecklistItemCheckedIcon width={20} height={20} />
) : (
<ChecklistItemUncheckedIcon width={20} height={20} />
)}
</ChecklistIcon>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(itemID, currentName);
setEditting(false);
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(itemID, currentName);
setEditting(false);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
<Spacer />
<EditableDeleteButton
onClick={e => {
e.stopPropagation();
setEditting(false);
onDeleteItem(itemID);
}}
>
<Trash width={16} height={16} />
</EditableDeleteButton>
</EditControls>
</>
) : (
<ChecklistItemDetails
onClick={() => {
setEditting(true);
}}
>
<ChecklistItemRow>
<ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls>
<ControlButton>
<AssignUserButton width={14} height={14} />
</ControlButton>
<ControlButton>
<ClockButton width={14} height={14} />
</ControlButton>
<ControlButton
onClick={e => {
e.stopPropagation();
onDeleteItem(itemID);
}}
>
<TrashButton width={14} height={14} />
</ControlButton>
</ChecklistControls>
</ChecklistItemTextControls>
</ChecklistItemRow>
</ChecklistItemDetails>
)}
</ChecklistItemWrapper>
);
};
type AddNewItemProps = {
onAddItem: (name: string) => void;
};
const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => {
const $editor = useRef<HTMLTextAreaElement>(null);
const $wrapper = useRef<HTMLDivElement>(null);
const [currentName, setCurrentName] = useState('');
const [editting, setEditting] = useState(false);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($wrapper, true, () => setEditting(false), null);
return (
<ChecklistNewItem ref={$wrapper}>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
onAddItem(currentName);
setCurrentName('');
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onAddItem(currentName);
setCurrentName('');
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
) : (
<NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton>
)}
</ChecklistNewItem>
);
};
type ChecklistTitleEditorProps = {
name: string;
onChangeName: (item: string) => void;
onCancel: () => void;
};
const ChecklistTitleEditor = React.forwardRef(
({ name, onChangeName, onCancel }: ChecklistTitleEditorProps, $name: any) => {
const [currentName, setCurrentName] = useState(name);
return (
<>
<ChecklistNameEditor
ref={$name}
value={currentName}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(currentName);
}
}}
/>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(currentName);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
onCancel();
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
);
},
);
type ChecklistProps = {
checklistID: string;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
name: string;
onChangeName: (item: string) => void;
onToggleItem: (taskID: string, complete: boolean) => void;
onChangeItemName: (itemID: string, currentName: string) => void;
onDeleteItem: (itemID: string) => void;
onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>;
};
const Checklist: React.FC<ChecklistProps> = ({
checklistID,
onDeleteChecklist,
name,
items,
onToggleItem,
onAddItem,
onChangeItemName,
onChangeName,
onDeleteItem,
}) => {
const $name = useRef<HTMLTextAreaElement>(null);
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
const [editting, setEditting] = useState(false);
// useOnOutsideClick($name, true, () => setEditting(false), null);
useEffect(() => {
if (editting && $name && $name.current) {
$name.current.focus();
$name.current.select();
}
}, [editting]);
return (
<Wrapper>
<WindowTitle>
<WindowTitleIcon width={24} height={24} />
{editting ? (
<ChecklistTitleEditor
ref={$name}
name={name}
onChangeName={currentName => {
onChangeName(currentName);
setEditting(false);
}}
onCancel={() => {
setEditting(false);
}}
/>
) : (
<WindowChecklistTitle>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<WindowOptions>
<DeleteButton
onClick={$target => {
onDeleteChecklist($target, checklistID);
}}
color="danger"
variant="outline"
>
Delete
</DeleteButton>
</WindowOptions>
</WindowChecklistTitle>
)}
</WindowTitle>
<ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
<ChecklistProgressBar>
<ChecklistProgressBarCurrent width={percent} />
</ChecklistProgressBar>
</ChecklistProgress>
<ChecklistItems>
{items
.slice()
.sort((a, b) => a.position - b.position)
.map(item => (
<ChecklistItem
key={item.id}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={onToggleItem}
/>
))}
</ChecklistItems>
<AddNewItem onAddItem={onAddItem} />
</Wrapper>
);
};
export default Checklist;

View File

@ -0,0 +1,65 @@
import React, { createRef, useState } from 'react';
import styled from 'styled-components';
import { action } from '@storybook/addon-actions';
import DropdownMenu from '.';
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
onAdminConsole={action('admin')}
onCloseDropdown={() => {
setMenu({ top: 0, left: 0, isOpen: false });
}}
onLogout={action('on logout')}
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;
position: absolute;
padding-top: 10px;
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);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
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,67 @@
import React, { useRef } from 'react';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Exit, User, Cog } from 'shared/icons';
import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem, ActionTitle } from './Styles';
type DropdownMenuProps = {
left: number;
top: number;
onLogout: () => void;
onCloseDropdown: () => void;
onAdminConsole: () => void;
};
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onCloseDropdown, onAdminConsole }) => {
const $containerRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($containerRef, true, onCloseDropdown, null);
return (
<Container ref={$containerRef} left={left} top={top}>
<Wrapper>
<ActionItem onClick={onAdminConsole}>
<User size={16} color="#c2c6dc" />
<ActionTitle>Profile</ActionTitle>
</ActionItem>
<Separator />
<ActionsList>
<ActionItem onClick={onLogout}>
<Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle>
</ActionItem>
</ActionsList>
</Wrapper>
<WrapperDiamond />
</Container>
);
};
type ProfileMenuProps = {
onProfile: () => void;
onLogout: () => void;
onAdminConsole: () => void;
};
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onAdminConsole, onProfile, onLogout }) => {
return (
<>
<ActionItem onClick={onAdminConsole}>
<Cog size={16} color="#c2c6dc" />
<ActionTitle>Admin Console</ActionTitle>
</ActionItem>
<Separator />
<ActionItem onClick={onProfile}>
<User size={16} color="#c2c6dc" />
<ActionTitle>Profile</ActionTitle>
</ActionItem>
<ActionsList>
<ActionItem onClick={onLogout}>
<Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle>
</ActionItem>
</ActionsList>
</>
);
};
export { ProfileMenu };
export default DropdownMenu;

View File

@ -0,0 +1,74 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import styled, { ThemeProvider } from 'styled-components';
import { Popup } from '../PopupMenu';
import DueDateManager from '.';
const PopupWrapper = styled.div`
width: 310px;
`;
export default {
component: DueDateManager,
title: 'DueDateManager',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<ThemeProvider theme={theme}>
<PopupWrapper>
<Popup title={null} tab={0}>
<DueDateManager
task={{
id: '1',
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
id: '1',
profileIcon: { url: null, initials: null, bgColor: null },
fullName: 'Jordan Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
onRemoveDueDate={action('remove due date')}
/>
</Popup>
</PopupWrapper>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,134 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
import Input from 'shared/components/Input';
export const Wrapper = styled.div`
display: flex
flex-direction: column;
& .react-datepicker {
background: #262c49;
font-family: 'Droid Sans', sans-serif;
border: none;
}
& .react-datepicker__triangle {
display: none;
}
& .react-datepicker-popper {
z-index: 10000;
margin-top: 0;
}
& .react-datepicker-time__header {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-list-item {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover {
color: rgba(${props => props.theme.colors.text.secondary});
background: rgba(${props => props.theme.colors.bg.secondary});
}
& .react-datepicker__time-container .react-datepicker__time {
background: rgba(${props => props.theme.colors.bg.primary});
}
& .react-datepicker--time-only {
background: rgba(${props => props.theme.colors.bg.primary});
border: 1px solid rgba(${props => props.theme.colors.border});
}
& .react-datepicker * {
box-sizing: content-box;
}
& .react-datepicker__day-name {
color: #c2c6dc;
outline: none;
box-shadow: none;
padding: 4px;
font-size: 12px40px
line-height: 40px;
}
& .react-datepicker__day-name:hover {
background: #10163a;
}
& .react-datepicker__month {
margin: 0;
}
& .react-datepicker__day,
& .react-datepicker__time-name {
color: #c2c6dc;
outline: none;
box-shadow: none;
padding: 4px;
font-size: 14px;
}
& .react-datepicker__day--outside-month {
opacity: 0.6;
}
& .react-datepicker__day:hover {
border-radius: 50%;
background: #10163a;
}
& .react-datepicker__day--selected {
border-radius: 50%;
background: rgba(115, 103, 240);
color: #fff;
}
& .react-datepicker__day--selected:hover {
border-radius: 50%;
background: rgba(115, 103, 240);
color: #fff;
}
& .react-datepicker__header {
background: none;
border: none;
}
& .react-datepicker__header--time {
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
}
`;
export const DueDatePickerWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
export const ConfirmAddDueDate = styled(Button)`
margin: 0 4px 0 0;
padding: 6px 12px;
`;
export const RemoveDueDate = styled(Button)`
padding: 6px 12px;
margin: 0 0 0 4px;
`;
export const CancelDueDate = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;
export const DueDateInput = styled(Input)`
margin-top: 15px;
margin-bottom: 5px;
padding-right: 10px;
`;
export const ActionWrapper = styled.div`
padding-top: 8px;
width: 100%;
display: flex;
justify-content: space-between;
`;

View File

@ -0,0 +1,290 @@
import React, { useState, useEffect, forwardRef } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
import _ from 'lodash';
import {
Wrapper,
ActionWrapper,
RemoveDueDate,
DueDateInput,
DueDatePickerWrapper,
ConfirmAddDueDate,
CancelDueDate,
} from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns';
import { useForm } from 'react-hook-form';
type DueDateManagerProps = {
task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void;
onRemoveDueDate: (task: Task) => void;
onCancel: () => void;
};
const Form = styled.form`
padding-top: 25px;
`;
const FormField = styled.div`
width: 50%;
display: inline-block;
`;
const HeaderSelectLabel = styled.div`
display: inline-block;
position: relative;
z-index: 9999;
border-radius: 3px;
cursor: pointer;
padding: 6px 10px;
text-decoration: underline;
margin: 6px 0;
font-size: 14px;
line-height: 16px;
margin-left: 0;
margin-right: 0;
padding-left: 4px;
padding-right: 4px;
color: #c2c6dc;
&:hover {
background: rgba(115, 103, 240);
color: #c2c6dc;
}
`;
const HeaderSelect = styled.select`
text-decoration: underline;
font-size: 14px;
text-align: center;
padding: 4px 6px;
background: none;
outline: none;
border: none;
border-radius: 3px;
appearance: none;
&:hover {
background: #262c49;
border: 1px solid rgba(115, 103, 240);
outline: none !important;
box-shadow: none;
color: #c2c6dc;
}
&::-ms-expand {
display: none;
}
cursor: pointer;
position: absolute;
z-index: 9998;
margin: 0;
left: 0;
top: 5px;
opacity: 0;
`;
const HeaderButton = styled.button`
cursor: pointer;
color: #c2c6dc;
text-decoration: underline;
font-size: 14px;
text-align: center;
padding: 6px 10px;
margin: 6px 0;
background: none;
outline: none;
border: none;
border-radius: 3px;
&:hover {
background: rgba(115, 103, 240);
color: #fff;
}
`;
const HeaderActions = styled.div`
position: relative;
text-align: center;
& > button:first-child {
float: left;
}
& > button:last-child {
float: right;
}
`;
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const now = moment();
const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
const [startDate, setStartDate] = useState(new Date());
useEffect(() => {
setTextStartDate(moment(startDate).format('YYYY-MM-DD'));
}, [startDate]);
const [textEndTime, setTextEndTime] = useState(now.format('h:mm A'));
const [endTime, setEndTime] = useState(now.toDate());
useEffect(() => {
setTextEndTime(moment(endTime).format('h:mm A'));
}, [endTime]);
const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const { register, handleSubmit, errors, setValue, setError, formState } = useForm<DueDateFormData>();
const saveDueDate = (data: any) => {
console.log(data);
const newDate = moment(`${data.endDate} ${data.endTime}`, 'YYYY-MM-DD h:mm A');
if (newDate.isValid()) {
onDueDateChange(task, newDate.toDate());
}
};
console.log(errors);
register({ name: 'endTime' }, { required: 'End time is required' });
useEffect(() => {
setValue('endTime', now.format('h:mm A'));
}, []);
const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => {
return (
<DueDateInput
id="endTime"
name="endTime"
ref={$ref}
onChange={e => {
console.log(`onCahnge ${e.currentTarget.value}`);
setTextEndTime(e.currentTarget.value);
setValue('endTime', e.currentTarget.value);
}}
width="100%"
variant="alternate"
label="Date"
onClick={onClick}
value={value}
/>
);
});
console.log(`textStartDate ${textStartDate}`);
return (
<Wrapper>
<Form onSubmit={handleSubmit(saveDueDate)}>
<FormField>
<DueDateInput
id="endDate"
name="endDate"
width="100%"
variant="alternate"
label="Date"
onChange={e => {
setTextStartDate(e.currentTarget.value);
}}
value={textStartDate}
ref={register({
required: 'End date is required.',
})}
/>
</FormField>
<FormField>
<DatePicker
selected={endTime}
onChange={date => {
const changedDate = moment(date ?? new Date());
console.log(`changed ${date}`);
setEndTime(changedDate.toDate());
setValue('endTime', changedDate.format('h:mm A'));
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat="h:mm aa"
customInput={<CustomTimeInput />}
/>
</FormField>
<DueDatePickerWrapper>
<DatePicker
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value))}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
selected={startDate}
inline
onChange={date => {
setStartDate(date ?? new Date());
}}
/>
</DueDatePickerWrapper>
<ActionWrapper>
<ConfirmAddDueDate type="submit" onClick={() => {}}>
Save
</ConfirmAddDueDate>
<RemoveDueDate
variant="outline"
color="danger"
onClick={() => {
onRemoveDueDate(task);
}}
>
Remove
</RemoveDueDate>
</ActionWrapper>
</Form>
</Wrapper>
);
};
export default DueDateManager;

View File

@ -0,0 +1,43 @@
import React from 'react';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import styled, { ThemeProvider } from 'styled-components';
import Input from '.';
import { User } from 'shared/icons';
export default {
component: Input,
title: 'Input',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const Wrapper = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
padding: 45px;
margin: 25px;
display: flex;
flex-direction: column;
`;
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<ThemeProvider theme={theme}>
<Wrapper>
<Input label="Label placeholder" />
<Input width="100%" placeholder="Placeholder" />
<Input icon={<User size={20} />} width="100%" placeholder="Placeholder" />
</Wrapper>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
import styled, { css } from 'styled-components/macro';
const InputWrapper = styled.div<{ width: string }>`
position: relative;
width: ${props => props.width};
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
justify-content: center;
margin-bottom: 2.2rem;
margin-top: 24px;
`;
const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width};
padding: 0.7rem !important;
color: #c2c6dc;
left: 0;
top: 0;
transition: all 0.2s ease;
position: absolute;
border-radius: 5px;
overflow: hidden;
font-size: 0.85rem;
cursor: text;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const InputInput = styled.input<{
hasValue: boolean;
hasIcon: boolean;
width: string;
focusBg: string;
borderColor: string;
}>`
width: ${props => props.width};
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${props => props.borderColor};
background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
${props => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
line-height: 16px;
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
&:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240);
background: ${props => props.focusBg};
}
&:focus ~ ${InputLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
${props =>
props.hasValue &&
css`
& ~ ${InputLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
`}
`;
const Icon = styled.div`
display: flex;
left: 16px;
position: absolute;
`;
type InputProps = {
variant?: 'normal' | 'alternate';
label?: string;
width?: string;
floatingLabel?: boolean;
placeholder?: string;
icon?: JSX.Element;
autocomplete?: boolean;
id?: string;
name?: string;
className?: string;
value?: string;
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const Input = React.forwardRef(
(
{
width = 'auto',
variant = 'normal',
autocomplete,
label,
placeholder,
icon,
name,
onChange,
className,
onClick,
floatingLabel,
value: initialValue,
id,
}: InputProps,
$ref: any,
) => {
const [value, setValue] = useState(initialValue ?? '');
useEffect(() => {
if (initialValue) {
setValue(initialValue);
}
}, [initialValue]);
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (onChange) {
onChange(e);
}
};
return (
<InputWrapper className={className} width={width}>
<InputInput
hasValue={floatingLabel || value !== ''}
ref={$ref}
id={id}
name={name}
onClick={onClick}
onChange={handleChange}
autoComplete={autocomplete ? 'on' : 'off'}
value={value}
hasIcon={typeof icon !== 'undefined'}
width={width}
placeholder={placeholder}
focusBg={focusBg}
borderColor={borderColor}
/>
{label && <InputLabel width={width}>{label}</InputLabel>}
<Icon>{icon && icon}</Icon>
</InputWrapper>
);
},
);
export default Input;

View File

@ -0,0 +1,177 @@
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 '.';
export default {
component: List,
title: 'List',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const labelData: Array<ProjectLabel> = [
{
id: 'development',
name: 'Development',
createdDate: new Date().toString(),
labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
},
];
const createCard = () => {
const $ref = createRef<HTMLDivElement>();
return (
<Card
taskID="1"
taskGroupID="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')}
onExtraMenuOpen={action('extra menu open')}
>
<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')}
onExtraMenuOpen={action('extra menu open')}
>
<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')}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<Card
taskID="1"
taskGroupID="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')}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<Card
taskID="1"
taskGroupID="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,128 @@
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: #c2c6dc;
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 {
color: #c2c6dc;
text-decoration: none;
background: rgb(115, 103, 240);
}
`;
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';
font-size: 14px;
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} {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
}
`}
`;
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: 45px;
overflow-y: auto;
overflow-x: hidden;
`;
export const ListExtraMenuButtonWrapper = styled.div`
cursor: pointer;
position: absolute;
right: 4px;
top: 4px;
z-index: 1;
padding: 6px;
padding-bottom: 0;
`;

View File

@ -0,0 +1,125 @@
import React, { useState, useRef } from 'react';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { Plus, Ellipsis } from 'shared/icons';
import {
Container,
Wrapper,
Header,
HeaderName,
HeaderEditTarget,
AddCardContainer,
AddCardButton,
AddCardButtonText,
ListCards,
ListExtraMenuButtonWrapper,
} from './Styles';
type Props = {
children: React.ReactNode;
id: string;
name: string;
onSaveName: (name: string) => void;
isComposerOpen: boolean;
onOpenComposer: (id: string) => void;
wrapperProps?: any;
headerProps?: any;
index?: number;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const List = React.forwardRef(
(
{
id,
name,
onSaveName,
isComposerOpen,
onOpenComposer,
children,
wrapperProps,
headerProps,
onExtraMenuOpen,
}: Props,
$wrapperRef: any,
) => {
const [listName, setListName] = useState(name);
const [isEditingTitle, setEditingTitle] = useState(false);
const $listNameRef = useRef<HTMLTextAreaElement>(null);
const $extraActionsRef = useRef<HTMLDivElement>(null);
const onClick = () => {
setEditingTitle(true);
if ($listNameRef && $listNameRef.current) {
$listNameRef.current.select();
}
};
const onBlur = () => {
setEditingTitle(false);
onSaveName(listName);
};
const onEscape = () => {
if ($listNameRef && $listNameRef.current) {
$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();
if ($listNameRef && $listNameRef.current) {
$listNameRef.current.blur();
}
}
};
const handleExtraMenuOpen = () => {
if ($extraActionsRef && $extraActionsRef.current) {
onExtraMenuOpen(id, $extraActionsRef);
}
};
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}
/>
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
<Ellipsis size={16} color="#c2c6dc" />
</ListExtraMenuButtonWrapper>
</Header>
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<Plus size={12} color="#c2c6dc" />
<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,18 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import ListActions from '.';
export default {
component: ListActions,
title: 'ListActions',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
export const Default = () => {
return <ListActions taskGroupID="1" onArchiveTaskGroup={action('on archive task group')} />;
};

View File

@ -0,0 +1,35 @@
import styled from 'styled-components';
export const ListActionsWrapper = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`;
export const ListActionItemWrapper = styled.li`
margin: 0;
padding: 0;
`;
export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #c2c6dc;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const ListSeparator = styled.hr`
background-color: #414561;
border: 0;
height: 1px;
margin: 8px 0;
padding: 0;
width: 100%;
`;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles';
type Props = {
taskGroupID: string;
onArchiveTaskGroup: (taskGroupID: string) => void;
};
const LabelManager: React.FC<Props> = ({ taskGroupID, onArchiveTaskGroup }) => {
return (
<>
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Add card...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Copy List...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Move card...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Watch</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Sort By...</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Move All Cards in This List...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Archive All Cards in This List...</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
<ListActionItem>Archive This List</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
</>
);
};
export default LabelManager;

View File

@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import Lists from '.';
export default {
component: Lists,
title: 'Lists',
parameters: {
backgrounds: [
{ name: 'gray', value: '#262c49', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
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',
taskGroup: { taskGroupID: 'column-1' },
name: 'Create roadmap',
position: 2,
labels: [],
},
'task-2': {
taskID: 'task-2',
taskGroup: { taskGroupID: 'column-1' },
position: 1,
name: 'Create authentication',
labels: [],
},
'task-3': {
taskID: 'task-3',
taskGroup: { taskGroupID: 'column-1' },
position: 3,
name: 'Create login',
labels: [],
},
'task-4': {
taskID: 'task-4',
taskGroup: { taskGroupID: 'column-1' },
position: 4,
name: 'Create plugins',
labels: [],
},
},
};
export const Default = () => {
const [listsData, setListsData] = useState(initialListsData);
const onCardDrop = (droppedTask: Task) => {
const newState = {
...listsData,
tasks: {
...listsData.tasks,
[droppedTask.id]: droppedTask,
},
};
setListsData(newState);
};
const onListDrop = (droppedColumn: any) => {
const newState = {
...listsData,
columns: {
...listsData.columns,
[droppedColumn.taskGroupID]: droppedColumn,
},
};
setListsData(newState);
};
return (
<Lists
taskGroups={[]}
onTaskClick={action('card click')}
onQuickEditorOpen={action('card composer open')}
onCreateTask={action('card create')}
onTaskDrop={onCardDrop}
onTaskGroupDrop={onListDrop}
onChangeTaskGroupName={action('change group name')}
onCreateTaskGroup={action('create list')}
onExtraMenuOpen={action('extra menu open')}
onCardMemberClick={action('card member click')}
/>
);
};

View File

@ -0,0 +1,48 @@
import styled from 'styled-components';
export const Container = styled.div`
flex: 1;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
::-webkit-scrollbar {
height: 10px;
}
::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 6px;
}
::-webkit-scrollbar-track {
background: #10163a;
border-radius: 6px;
}
`;
export const BoardContainer = styled.div`
position: relative;
overflow-y: auto;
outline: none;
flex-grow: 1;
`;
export const BoardWrapper = styled.div`
display: flex;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`;
export default Container;

View File

@ -0,0 +1,228 @@
import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import List, { ListCards } from 'shared/components/List';
import Card from 'shared/components/Card';
import CardComposer from 'shared/components/CardComposer';
import AddList from 'shared/components/AddList';
import {
isPositionChanged,
getSortedDraggables,
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
import moment from 'moment';
import { Container, BoardContainer, BoardWrapper } from './Styles';
interface SimpleProps {
taskGroups: Array<TaskGroup>;
onTaskDrop: (task: Task, previousTaskGroupID: string) => void;
onTaskGroupDrop: (taskGroup: TaskGroup) => void;
onTaskClick: (task: Task) => void;
onCreateTask: (taskGroupID: string, name: string) => void;
onChangeTaskGroupName: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
onCreateTaskGroup: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
onCardMemberClick: OnCardMemberClick;
}
const SimpleLists: React.FC<SimpleProps> = ({
taskGroups,
onTaskDrop,
onChangeTaskGroupName,
onTaskGroupDrop,
onTaskClick,
onCreateTask,
onQuickEditorOpen,
onCreateTaskGroup,
onExtraMenuOpen,
onCardMemberClick,
}) => {
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;
let droppedDraggable: DraggableElement | null = null;
let beforeDropDraggables: Array<DraggableElement> | null = null;
if (isList) {
const droppedGroup = taskGroups.find(taskGroup => taskGroup.id === draggableId);
if (droppedGroup) {
droppedDraggable = {
id: draggableId,
position: droppedGroup.position,
};
beforeDropDraggables = getSortedDraggables(
taskGroups.map(taskGroup => {
return { id: taskGroup.id, position: taskGroup.position };
}),
);
if (droppedDraggable === null || beforeDropDraggables === null) {
throw new Error('before drop draggables is null');
}
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
isList,
isSameList,
destination,
);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
onTaskGroupDrop({
...droppedGroup,
position: newPosition,
});
} else {
throw { error: 'task group can not be found' };
}
} else {
const targetGroup = taskGroups.findIndex(
taskGroup => taskGroup.tasks.findIndex(task => task.id === draggableId) !== -1,
);
const droppedTask = taskGroups[targetGroup].tasks.find(task => task.id === draggableId);
if (droppedTask) {
droppedDraggable = {
id: draggableId,
position: droppedTask.position,
};
beforeDropDraggables = getSortedDraggables(
taskGroups[targetGroup].tasks.map(task => {
return { id: task.id, position: task.position };
}),
);
if (droppedDraggable === null || beforeDropDraggables === null) {
throw new Error('before drop draggables is null');
}
const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables,
droppedDraggable,
isList,
isSameList,
destination,
);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
const newTask = {
...droppedTask,
position: newPosition,
taskGroup: {
id: destination.droppableId,
},
};
onTaskDrop(newTask, droppedTask.taskGroup.id);
}
}
};
const [currentComposer, setCurrentComposer] = useState('');
return (
<BoardContainer>
<BoardWrapper>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root">
{provided => (
<Container {...provided.droppableProps} ref={provided.innerRef}>
{taskGroups
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((taskGroup: TaskGroup, index: number) => {
return (
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
{columnDragProvided => (
<Droppable type="tasks" droppableId={taskGroup.id}>
{(columnDropProvided, snapshot) => (
<List
name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={onExtraMenuOpen}
id={taskGroup.id}
key={taskGroup.id}
index={index}
>
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((task: Task, taskIndex: any) => {
return (
<Draggable key={task.id} draggableId={task.id} index={taskIndex}>
{taskProvided => {
return (
<Card
wrapperProps={{
...taskProvided.draggableProps,
...taskProvided.dragHandleProps,
}}
ref={taskProvided.innerRef}
taskID={task.id}
complete={task.complete ?? false}
taskGroupID={taskGroup.id}
description=""
labels={task.labels.map(label => label.projectLabel)}
dueDate={
task.dueDate
? {
isPastDue: false,
formattedDate: moment(task.dueDate).format('MMM D, YYYY'),
}
: undefined
}
title={task.name}
members={task.assigned}
onClick={() => {
onTaskClick(task);
}}
checklists={task.badges && task.badges.checklist}
onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen}
/>
);
}}
</Draggable>
);
})}
{columnDropProvided.placeholder}
{currentComposer === taskGroup.id && (
<CardComposer
onClose={() => {
setCurrentComposer('');
}}
onCreateCard={name => {
onCreateTask(taskGroup.id, name);
}}
isOpen
/>
)}
</ListCards>
</List>
)}
</Droppable>
)}
</Draggable>
);
})}
<AddList
onSave={listName => {
onCreateTaskGroup(listName);
}}
/>
{provided.placeholder}
</Container>
)}
</Droppable>
</DragDropContext>
</BoardWrapper>
</BoardContainer>
);
};
export default SimpleLists;

View File

@ -0,0 +1,67 @@
import React 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 '.';
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';
import Button from 'shared/components/Button';
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(Button)``;
export const ActionButtons = styled.div`
margin-top: 17.5px;
display: flex;
justify-content: space-between;
`;
export const RegisterButton = styled(Button)``;
export const LogoTitle = styled.div`
font-size: 24px;
font-weight: 600;
margin-left: 12px;
transition: visibility, opacity, transform 0.25s ease;
color: #7367f0;
`;
export const LogoWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
position: relative;
width: 100%;
padding-bottom: 16px;
margin-bottom: 24px;
color: rgb(222, 235, 255);
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;

View File

@ -0,0 +1,88 @@
import React, { useState } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock, Citadel } from 'shared/icons';
import { useForm } from 'react-hook-form';
import {
Form,
LogoWrapper,
LogoTitle,
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>();
const loginSubmit = (data: LoginFormData) => {
setComplete(false);
onSubmit(data, setComplete, setError);
};
return (
<Wrapper>
<Column>
<AccessAccount width={275} height={250} />
</Column>
<Column>
<LoginFormWrapper>
<LoginFormContainer>
<LogoWrapper>
<Citadel width={42} height={42} />
<LogoTitle>Citadel</LogoTitle>
</LogoWrapper>
<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="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password && <FormError>{errors.password.message}</FormError>}
<ActionButtons>
<RegisterButton variant="outline">Register</RegisterButton>
<LoginButton type="submit" disabled={!isComplete}>
Login
</LoginButton>
</ActionButtons>
</Form>
</LoginFormContainer>
</LoginFormWrapper>
</Column>
</Wrapper>
);
};
export default Login;

View File

@ -0,0 +1,71 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
const CardMember = styled.div<{ bgColor: string }>`
height: 28px;
width: 28px;
float: right;
margin: 0 0 4px 4px;
background-color: ${props => props.bgColor};
color: #fff;
border-radius: 25em;
cursor: pointer;
display: block;
overflow: visible;
position: relative;
text-decoration: none;
z-index: 0;
`;
const CardMemberInitials = styled.div`
height: 28px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 400;
`;
type MemberProps = {
onCardMemberClick?: OnCardMemberClick;
taskID?: string;
member: TaskUser;
showName?: boolean;
className?: string;
};
const CardMemberWrapper = styled.div<{ ref: any }>`
display: flex;
align-items: center;
`;
const CardMemberName = styled.span`
font-size: 16px;
padding-left: 8px;
`;
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member, showName, className }) => {
const $targetRef = useRef<HTMLDivElement>();
return (
<CardMemberWrapper
key={member.id}
ref={$targetRef}
className={className}
onClick={e => {
if (onCardMemberClick) {
e.stopPropagation();
onCardMemberClick($targetRef, taskID ?? '', member.id);
}
}}
>
<CardMember bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
{showName && <CardMemberName>{member.fullName}</CardMemberName>}
</CardMemberWrapper>
);
};
export default Member;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import MemberManager from '.';
export default {
component: MemberManager,
title: 'MemberManager',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
export const Default = () => {
return <MemberManager availableMembers={[]} activeMembers={[]} onMemberChange={action('member change')} />;
};

View File

@ -0,0 +1,92 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const MemberManagerWrapper = styled.div``;
export const MemberManagerSearchWrapper = styled.div`
width: 100%;
display: flex;
`;
export const MemberManagerSearch = styled(TextareaAutosize)`
margin: 4px 0 12px;
width: 100%;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
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 BoardMembersLabel = styled.h4`
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 16px;
text-transform: uppercase;
`;
export const BoardMembersList = styled.ul`
margin: 0;
padding: 0;
list-style-type: none;
`;
export const BoardMembersListItem = styled.li``;
export const BoardMemberListItemContent = styled.div`
background-color: rgba(9, 30, 66, 0.04);
padding-right: 28px;
border-radius: 3px;
display: flex;
height: 40px;
overflow: hidden;
cursor: pointer;
align-items: center;
position: relative;
text-overflow: ellipsis;
text-decoration: none;
white-space: nowrap;
padding: 4px;
margin-bottom: 2px;
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #c2c6dc;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
margin-right: 6px;
`;
export const MemberName = styled.span`
font-size: 14px;
`;
export const ActiveIconWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 11px;
`;

View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import {
MemberName,
ProfileIcon,
MemberManagerWrapper,
MemberManagerSearchWrapper,
MemberManagerSearch,
BoardMembersLabel,
BoardMembersList,
BoardMembersListItem,
BoardMemberListItemContent,
ActiveIconWrapper,
} from './Styles';
import { Checkmark } from 'shared/icons';
type MemberManagerProps = {
availableMembers: Array<TaskUser>;
activeMembers: Array<TaskUser>;
onMemberChange: (member: TaskUser, isActive: boolean) => void;
};
const MemberManager: React.FC<MemberManagerProps> = ({
availableMembers,
activeMembers: initialActiveMembers,
onMemberChange,
}) => {
const [activeMembers, setActiveMembers] = useState(initialActiveMembers);
const [currentSearch, setCurrentSearch] = useState('');
return (
<MemberManagerWrapper>
<MemberManagerSearchWrapper>
<MemberManagerSearch
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setCurrentSearch(e.currentTarget.value);
}}
/>
</MemberManagerSearchWrapper>
<BoardMembersLabel>Board Members</BoardMembersLabel>
<BoardMembersList>
{availableMembers
.filter(
member => currentSearch === '' || member.fullName.toLowerCase().startsWith(currentSearch.toLowerCase()),
)
.map(member => {
return (
<BoardMembersListItem key={member.id}>
<BoardMemberListItemContent
onClick={() => {
const isActive = activeMembers.findIndex(m => m.id === member.id) !== -1;
if (isActive) {
setActiveMembers(activeMembers.filter(m => m.id !== member.id));
} else {
setActiveMembers([...activeMembers, member]);
}
onMemberChange(member, !isActive);
}}
>
<ProfileIcon>JK</ProfileIcon>
<MemberName>{member.fullName}</MemberName>
{activeMembers.findIndex(m => m.id === member.id) !== -1 && (
<ActiveIconWrapper>
<Checkmark width={16} height={16} />
</ActiveIconWrapper>
)}
</BoardMemberListItemContent>
</BoardMembersListItem>
);
})}
</BoardMembersList>
</MemberManagerWrapper>
);
};
export default MemberManager;

View File

@ -0,0 +1,132 @@
import styled, { css } from 'styled-components';
import Button from 'shared/components/Button';
import { Checkmark } from 'shared/icons';
export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px;
`;
export const Profile = styled.div`
margin: 8px 0;
min-height: 56px;
position: relative;
`;
export const ProfileIcon = styled.div<{ bgUrl: string | null; bgColor: string }>`
float: left;
margin: 2px;
background: ${props => (props.bgUrl ? `url(${props.bgUrl})` : props.bgColor)};
background-position: center;
background-size: contain;
border-radius: 25em;
font-size: 16px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
overflow: hidden;
position: relative;
width: 50px;
z-index: 1;
`;
export const ProfileInfo = styled.div`
color: #c2c6dc;
margin: 0 0 0 64px;
word-wrap: break-word;
`;
export const InfoTitle = styled.h3`
margin: 0 40px 0 0;
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #c2c6dc;
`;
export const InfoUsername = styled.p`
color: #c2c6dc;
font-size: 14px;
line-height: 20px;
`;
export const InfoBio = styled.p`
font-size: 14px;
line-height: 20px;
color: #c2c6dc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
padding: 0;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
color: #c2c6dc;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
${props =>
props.disabled
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
`
: css`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`}
`;
export const CurrentPermission = styled.span`
margin-left: 4px;
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
`;
export const Separator = styled.div`
height: 1px;
border-top: 1px solid #414561;
margin: 0.25rem !important;
`;
export const WarningText = styled.span`
display: flex;
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
padding: 6px;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const RemoveMemberButton = styled(Button)`
margin-top: 16px;
padding: 6px 12px;
width: 100%;
`;
export const Content = styled.div`
padding: 0 12px 12px;
`;
export const RoleName = styled.div`
font-size: 14px;
font-weight: 700;
`;
export const RoleDescription = styled.div`
margin-top: 4px;
font-size: 14px;
`;

View File

@ -0,0 +1,218 @@
import React from 'react';
import { Popup, usePopup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql';
import {
RoleCheckmark,
RoleName,
RoleDescription,
Profile,
Content,
DeleteDescription,
RemoveMemberButton,
WarningText,
ProfileIcon,
Separator,
ProfileInfo,
InfoTitle,
InfoUsername,
InfoBio,
CurrentPermission,
MiniProfileActions,
MiniProfileActionWrapper,
MiniProfileActionItem,
} from './Styles';
const permissions = [
{
code: 'owner',
name: 'Owner',
description:
'Can view and edit cards, remove members, and change all settings for the project. Can delete the project.',
},
{
code: 'admin',
name: 'Admin',
description: 'Can view and edit cards, remove members, and change all settings for the project.',
},
{ code: 'member', name: 'Member', description: "Can view and edit cards. Can't change settings." },
{
code: 'observer',
name: 'Observer',
description: "Can view, comment, and vote on cards. Can't move or edit cards or change settings.",
},
];
type MiniProfileProps = {
bio: string;
user: TaskUser;
onRemoveFromTask?: () => void;
onChangeRole?: (roleCode: RoleCode) => void;
onRemoveFromBoard?: () => void;
onChangeProjectOwner?: (userID: string) => void;
warning?: string | null;
canChangeRole?: boolean;
};
const MiniProfile: React.FC<MiniProfileProps> = ({
user,
bio,
canChangeRole,
onChangeProjectOwner,
onRemoveFromTask,
onChangeRole,
onRemoveFromBoard,
warning,
}) => {
const { hidePopup, setTab } = usePopup();
return (
<>
<Popup title={null} onClose={() => hidePopup()} tab={0}>
<Profile>
{user.profileIcon && (
<ProfileIcon bgUrl={user.profileIcon.url ?? null} bgColor={user.profileIcon.bgColor ?? ''}>
{user.profileIcon.url === null && user.profileIcon.initials}
</ProfileIcon>
)}
<ProfileInfo>
<InfoTitle>{user.fullName}</InfoTitle>
<InfoUsername>{`@${user.username}`}</InfoUsername>
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
{onRemoveFromTask && (
<MiniProfileActionItem
onClick={() => {
onRemoveFromTask();
}}
>
Remove from card
</MiniProfileActionItem>
)}
{onChangeProjectOwner && (
<MiniProfileActionItem
onClick={() => {
setTab(3);
}}
>
Set as project owner
</MiniProfileActionItem>
)}
{onChangeRole && user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
{onRemoveFromBoard && (
<MiniProfileActionItem
onClick={() => {
setTab(2);
}}
>
Remove from board...
</MiniProfileActionItem>
)}
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
case 'observer':
onChangeRole(RoleCode.Observer);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove Member?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</DeleteDescription>
<RemoveMemberButton
color="danger"
onClick={() => {
if (onRemoveFromBoard) {
onRemoveFromBoard();
}
}}
>
Remove Member
</RemoveMemberButton>
</Content>
</Popup>
<Popup title="Set as Project Owner?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
This will change the project owner from you to this user. They will be able to view and edit cards, remove
members, and change all settings for the project. They will also be able to delete the project.
</DeleteDescription>
<RemoveMemberButton
color="warning"
onClick={() => {
if (onChangeProjectOwner) {
onChangeProjectOwner(user.id);
}
}}
>
Set as Project Owner
</RemoveMemberButton>
</Content>
</Popup>
</>
);
};
export default MiniProfile;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Modal from '.';
export default {
component: Modal,
title: 'Modal',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Modal
width={1040}
onClose={action('on close')}
renderContent={() => {
return <h1>Hello!</h1>;
}}
/>
</>
);
};

View File

@ -0,0 +1,32 @@
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ScrollOverlay = styled.div`
z-index: 3000;
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
`;
export const ClickableOverlay = styled.div`
min-height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
`;
export const StyledModal = styled.div<{ width: number }>`
display: inline-block;
position: relative;
margin: 48px 0 80px;
width: 100%;
background: #262c49;
max-width: ${props => props.width}px;
vertical-align: middle;
border-radius: 3px;
${mixin.boxShadowMedium}
`;

View File

@ -0,0 +1,36 @@
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { ScrollOverlay, ClickableOverlay, StyledModal } from './Styles';
const $root: HTMLElement = document.getElementById('root')!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
type ModalProps = {
width: number;
onClose: () => void;
renderContent: () => JSX.Element;
};
const Modal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
const $modalRef = useRef<HTMLDivElement>(null);
const $clickableOverlayRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef);
useOnEscapeKeyDown(true, tellParentToClose);
return ReactDOM.createPortal(
<ScrollOverlay>
<ClickableOverlay ref={$clickableOverlayRef}>
<StyledModal width={width} ref={$modalRef}>
{renderContent()}
</StyledModal>
</ClickableOverlay>
</ScrollOverlay>,
$root,
);
};
export default Modal;

View File

@ -0,0 +1,40 @@
import React from 'react';
import styled from 'styled-components';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import { Home } from 'shared/icons';
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from '.';
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 width={28} height={28} />
</ActionButton>
<ActionButton name="Home">
<Home width={28} height={28} />
</ActionButton>
</ButtonContainer>
</Navbar>
</>
);
};

View File

@ -0,0 +1,111 @@
import styled, { css } from 'styled-components';
export const Logo = styled.div``;
export const LogoTitle = styled.div`
position: absolute;
visibility: hidden;
opacity: 0;
font-size: 24px;
font-weight: 600;
transition: 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;
& > a:first-child > div {
padding-top: 48px;
}
`;
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: 24px 15px;
display: flex;
align-items: center;
&:hover ${ActionButtonTitle} {
transform: translateX(5px);
}
&:hover ${IconWrapper} {
transform: translateX(5px);
}
`;
export const LogoWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
width: 100%;
height: 80px;
color: rgb(222, 235, 255);
cursor: pointer;
transition: color 0.1s ease 0s, border 0.1s ease 0s;
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;
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;
border-right: 1px solid rgba(65, 69, 97, 0.65);
&:hover {
width: 260px;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
border-right: 1px solid rgba(65, 69, 97, 0);
}
&:hover ${LogoTitle} {
bottom: -12px;
visibility: visible;
opacity: 1;
}
&:hover ${ActionButtonTitle} {
left: 15px;
visibility: visible;
opacity: 1;
}
&:hover ${LogoWrapper} {
border-bottom: 1px solid rgba(65, 69, 97, 0);
}
`;

View File

@ -0,0 +1,48 @@
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>
<Citadel width={42} height={42} />
<LogoTitle>Citadel</LogoTitle>
</LogoWrapper>
);
};
const Navbar: React.FC = ({ children }) => {
return <Container>{children}</Container>;
};
export default Navbar;

View File

@ -0,0 +1,33 @@
import React, { useState, useRef, createRef } from 'react';
import { action } from '@storybook/addon-actions';
import styled from 'styled-components';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import NewProject from '.';
export default {
component: NewProject,
title: 'NewProject',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<NewProject
initialTeamID={null}
onCreateProject={action('create project')}
teams={[{ name: 'General', id: 'general', createdAt: '' }]}
onClose={() => {}}
/>
</>
);
};

View File

@ -0,0 +1,281 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
import Select from 'react-select';
import { ArrowLeft, Cross } from 'shared/icons';
const Overlay = styled.div`
z-index: 10000;
background: #262c49;
bottom: 0;
color: #fff;
left: 0;
position: fixed;
right: 0;
top: 0;
`;
const Content = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const Header = styled.div`
height: 64px;
padding: 0 24px;
align-items: center;
display: flex;
flex: 0 0 auto;
justify-content: space-between;
transition: box-shadow 250ms;
`;
const HeaderLeft = styled.div`
align-items: center;
display: flex;
cursor: pointer;
`;
const HeaderRight = styled.div`
cursor: pointer;
align-items: center;
display: flex;
`;
const Container = styled.div`
padding: 32px 0;
align-items: center;
display: flex;
flex-direction: column;
`;
const ContainerContent = styled.div`
width: 520px;
display: flex;
flex-direction: column;
`;
const Title = styled.h1`
font-size: 24px;
font-weight: 500;
color: #c2c6dc;
margin-bottom: 25px;
`;
const ProjectName = styled.input`
margin: 0 0 12px;
width: 100%;
box-sizing: border-box;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
outline: none;
color: #c2c6dc;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
font-size: 16px;
font-weight: 400;
&:focus {
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
}
`;
const ProjectNameLabel = styled.label`
color: #c2c6dc;
font-size: 12px;
margin-bottom: 4px;
`;
const ProjectInfo = styled.div`
display: flex;
`;
const ProjectField = styled.div`
display: flex;
flex-direction: column;
margin-right: 15px;
flex-grow: 1;
`;
const ProjectTeamField = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
`;
const colourStyles = {
control: (styles: any, data: any) => {
return {
...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
},
':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)',
},
};
},
menu: (styles: any) => {
return {
...styles,
backgroundColor: mixin.darken('#262c49', 0.15),
};
},
dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
indicatorSeparator: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
option: (styles: any, { data, isDisabled, isFocused, isSelected }: any) => {
return {
...styles,
backgroundColor: isDisabled
? null
: isSelected
? mixin.darken('#262c49', 0.25)
: isFocused
? mixin.darken('#262c49', 0.15)
: null,
color: isDisabled ? '#ccc' : isSelected ? '#fff' : '#c2c6dc',
cursor: isDisabled ? 'not-allowed' : 'default',
':active': {
...styles[':active'],
backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'),
},
':hover': {
...styles[':hover'],
backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'),
},
};
},
placeholder: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
clearIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
input: (styles: any) => ({
...styles,
color: '#fff',
}),
singleValue: (styles: any) => {
return {
...styles,
color: '#fff',
};
},
};
const CreateButton = styled.button`
outline: none;
border: none;
width: 100%;
line-height: 20px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
font-size: 14px;
cursor: pointer;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
&:hover {
color: #fff;
background: rgb(115, 103, 240);
border-color: rgb(115, 103, 240);
}
`;
type NewProjectProps = {
initialTeamID: string | null;
teams: Array<Team>;
onClose: () => void;
onCreateProject: (projectName: string, teamID: string) => void;
};
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
const [projectName, setProjectName] = useState('');
const [team, setTeam] = useState<null | string>(initialTeamID);
const options = teams.map(t => ({ label: t.name, value: t.id }));
return (
<Overlay>
<Content>
<Header>
<HeaderLeft
onClick={() => {
onClose();
}}
>
<ArrowLeft color="#c2c6dc" />
</HeaderLeft>
<HeaderRight
onClick={() => {
onClose();
}}
>
<Cross width={16} height={16} />
</HeaderRight>
</Header>
<Container>
<ContainerContent>
<Title>Add project details</Title>
<ProjectInfo>
<ProjectField>
<ProjectNameLabel>Project name</ProjectNameLabel>
<ProjectName
value={projectName}
onChange={(e: any) => {
setProjectName(e.currentTarget.value);
}}
/>
</ProjectField>
<ProjectTeamField>
<ProjectNameLabel>Team</ProjectNameLabel>
<Select
onChange={(e: any) => {
setTeam(e.value);
}}
value={options.filter(d => d.value === team)}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}
/>
</ProjectTeamField>
</ProjectInfo>
<CreateButton
onClick={() => {
if (team && projectName !== '') {
onCreateProject(projectName, team);
}
}}
>
Create project
</CreateButton>
</ContainerContent>
</Container>
</Content>
</Overlay>
);
};
export default NewProject;

View File

@ -0,0 +1,80 @@
import React, { useState, useEffect, useRef } from 'react';
import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
import styled from 'styled-components';
const WhiteCheckmark = styled(Checkmark)`
fill: rgba(${props => props.theme.colors.text.secondary});
`;
type Props = {
labelColors: Array<LabelColor>;
label: ProjectLabel | null;
onLabelEdit: (labelId: string | null, labelName: string, labelColor: LabelColor) => void;
onLabelDelete?: (labelId: string) => void;
};
const LabelManager = ({ labelColors, label, onLabelEdit, onLabelDelete }: Props) => {
const $fieldName = useRef<HTMLInputElement>(null);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<LabelColor | null>(label ? label.labelColor : null);
useEffect(() => {
if ($fieldName.current) {
$fieldName.current.focus();
}
}, []);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName
ref={$fieldName}
id="labelName"
type="text"
name="name"
onChange={e => {
setCurrentLabel(e.currentTarget.value);
}}
value={currentLabel ?? ''}
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{labelColors.map((labelColor: LabelColor) => (
<LabelBox
key={labelColor.id}
color={labelColor.colorHex}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{currentColor && labelColor.id === currentColor.id && <WhiteCheckmark width={12} height={12} />}
</LabelBox>
))}
</div>
<div>
<SaveButton
value="Save"
type="submit"
onClick={e => {
e.preventDefault();
console.log(currentColor);
if (currentColor) {
onLabelEdit(label ? label.id : null, currentLabel ?? '', currentColor);
}
}}
/>
{label && onLabelDelete && (
<DeleteButton
value="Delete"
type="submit"
onClick={e => {
e.preventDefault();
onLabelDelete(label.id);
}}
/>
)}
</div>
</EditLabelForm>
);
};
export default LabelManager;

View File

@ -0,0 +1,93 @@
import React, { useState, useEffect, useRef } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import {
LabelSearch,
ActiveIcon,
Labels,
Label,
CardLabel,
Section,
SectionTitle,
LabelIcon,
CreateLabelButton,
} from './Styles';
type Props = {
labels?: Array<ProjectLabel>;
taskLabels?: Array<TaskLabel>;
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string) => void;
onLabelCreate: () => void;
};
const LabelManager: React.FC<Props> = ({ labels, taskLabels, onLabelToggle, onLabelEdit, onLabelCreate }) => {
const $fieldName = useRef<HTMLInputElement>(null);
const [currentLabel, setCurrentLabel] = useState('');
const [currentSearch, setCurrentSearch] = useState('');
useEffect(() => {
if ($fieldName.current) {
$fieldName.current.focus();
}
}, []);
return (
<>
<LabelSearch
type="text"
ref={$fieldName}
placeholder="search labels..."
onChange={e => {
setCurrentSearch(e.currentTarget.value);
}}
value={currentSearch}
/>
<Section>
<SectionTitle>Labels</SectionTitle>
<Labels>
{labels &&
labels
.filter(
label =>
currentSearch === '' ||
(label.name && label.name.toLowerCase().startsWith(currentSearch.toLowerCase())),
)
.map(label => (
<Label key={label.id}>
<LabelIcon
onClick={() => {
onLabelEdit(label.id);
}}
>
<Pencil width={16} height={16} />
</LabelIcon>
<CardLabel
key={label.id}
color={label.labelColor.colorHex}
active={currentLabel === label.id}
onMouseEnter={() => {
setCurrentLabel(label.id);
}}
onClick={() => onLabelToggle(label.id)}
>
{label.name}
{taskLabels && taskLabels.find(t => t.projectLabel.id === label.id) && (
<ActiveIcon>
<Checkmark width={16} height={16} />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
</Labels>
<CreateLabelButton
onClick={() => {
onLabelCreate();
}}
>
Create a new label
</CreateLabelButton>
</Section>
</>
);
};
export default LabelManager;

View File

@ -0,0 +1,382 @@
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';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
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 produce from 'immer';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import PopupMenu, { PopupProvider, usePopup, Popup } from '.';
export default {
component: PopupMenu,
title: 'PopupMenu',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const labelData: Array<ProjectLabel> = [
{
id: 'development',
name: 'Development',
createdDate: new Date().toString(),
labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
},
];
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.id === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx] };
}
}),
);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
label={labels.find(label => label.id === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.id === currentLabel);
if (idx !== -1) {
draftState[idx] = {
...draftState[idx],
name,
labelColor: {
...draftState[idx].labelColor,
name: color.name ?? '',
colorHex: color.colorHex,
},
};
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
onLabelEdit={(_labelId, name, color) => {
setLabels([
...labels,
{
id: name,
name,
createdDate: new Date().toString(),
labelColor: {
id: color.id,
colorHex: color.colorHex,
name: color.name ?? '',
position: 1,
},
},
]);
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 (
<PopupProvider>
<OpenLabelsButton />
</PopupProvider>
);
};
export const LabelsLabelEditor = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu
onPrevious={action('on previous')}
title="Change Label"
top={10}
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor
label={labelData[0]}
onLabelEdit={action('label edit')}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
/>
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
);
};
const initalState = { left: 0, top: 0, isOpen: false };
export const ListActionsPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<ListActions taskGroupID="1" onArchiveTaskGroup={action('archive task group')} />
</PopupMenu>
)}
<button
ref={$buttonRef}
type="submit"
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</button>
</>
);
};
export const MemberManagerPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Members" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<MemberManager
availableMembers={[
{
id: '1',
fullName: 'Jordan Knott',
profileIcon: { bgColor: null, url: null, initials: null },
},
]}
activeMembers={[]}
onMemberChange={action('member change')}
/>
</PopupMenu>
)}
<span
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};
export const DueDateManagerPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<DueDateManager
onRemoveDueDate={action('remove due date')}
task={{
id: '1',
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
id: '1',
profileIcon: { bgColor: null, url: null, initials: null },
fullName: 'Jordan Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};
export const MiniProfilePopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu
noHeader
title="Due Date"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<MiniProfile
user={{
id: '1',
fullName: 'Jordan Knott',
username: 'jordanthedev',
profileIcon: { url: null, bgColor: '#000', initials: 'JK' },
}}
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
color: '#fff',
background: '#f00',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};

View File

@ -0,0 +1,374 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{
invertY: boolean;
invert: boolean;
top: number;
left: number;
ref: any;
width: number | string;
}>`
left: ${props => props.left}px;
top: ${props => props.top}px;
display: block;
position: absolute;
width: ${props => props.width}px;
padding-top: 10px;
height: auto;
z-index: 40000;
${props =>
props.invert &&
css`
transform: translate(-100%);
`}
${props =>
props.invertY &&
css`
top: auto;
padding-top: 0;
padding-bottom: 10px;
bottom: ${props.top}px;
`}
`;
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`
height: 40px;
position: relative;
margin-bottom: 8px;
text-align: center;
`;
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #c2c6dc;
display: block;
border-bottom: 1px solid #414561;
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 1;
height: 40px;
line-height: 18px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
`;
export const Content = styled.div`
max-height: 632px;
overflow-x: hidden;
overflow-y: auto;
`;
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%;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
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`
margin-top: 12px;
`;
export const SectionTitle = styled.h4`
color: #c2c6dc;
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.12)};
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;
min-height: 31px;
`;
export const CloseButton = styled.div`
padding: 18px 18px 14px 12px;
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
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: 100%;
font-size: 16px;
line-height: 20px;
width: auto;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
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%;
box-sizing: border-box;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
outline: none;
color: #c2c6dc;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
font-size: 12px;
font-weight: 400;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const LabelBox = styled.span<{ color: string }>`
float: left;
height: 32px;
margin: 0 8px 8px 0;
padding: 0;
width: 48px;
cursor: pointer;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 0.8;
}
`;
export const SaveButton = styled.input`
background: rgb(115, 103, 240);
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;
`;
export const DeleteButton = styled.input`
float: right;
outline: none;
border: none;
line-height: 20px;
padding: 6px 12px;
background-color: transparent;
text-align: center;
color: #c2c6dc;
font-weight: 400;
line-height: 20px;
cursor: pointer;
margin: 0 0 0 8px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
&:hover {
color: #fff;
background: rgb(115, 103, 240);
border-color: transparent;
}
`;
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: 18px 18px 14px 12px;
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; invertY: boolean }>`
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute;
width: 10px;
height: 10px;
display: block;
${props =>
props.invertY
? css`
bottom: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-right: 1px solid rgba(0, 0, 0, 0.1);
`
: css`
top: 10px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
`}
transform: rotate(45deg) translate(-7px);
z-index: 10;
background: #262c49;
border-color: #414561;
`;

View File

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

View File

@ -0,0 +1,45 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
type ProfileIconProps = {
user: TaskUser;
onProfileClick: ($target: React.RefObject<HTMLElement>, user: TaskUser) => void;
size: number | string;
};
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
const $profileRef = useRef<HTMLDivElement>(null);
return (
<Container
ref={$profileRef}
onClick={() => {
onProfileClick($profileRef, user);
}}
size={size}
backgroundURL={user.profileIcon.url ?? null}
bgColor={user.profileIcon.bgColor ?? null}
>
{(!user.profileIcon.url && user.profileIcon.initials) ?? ''}
</Container>
);
};
ProfileIcon.defaultProps = {
size: 28,
};
export default ProfileIcon;

View File

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

View File

@ -0,0 +1,37 @@
import React from 'react';
import { Plus } from 'shared/icons';
import { AddProjectWrapper, AddProjectLabel, ProjectWrapper, ProjectContent, ProjectTitle, TeamTitle } from './Styles';
type AddProjectItemProps = {
onAddProject: () => void;
};
export const AddProjectItem: React.FC<AddProjectItemProps> = ({ onAddProject }) => {
return (
<AddProjectWrapper
onClick={() => {
onAddProject();
}}
>
<Plus size={20} color="#c2c6dc" />
<AddProjectLabel>New Project</AddProjectLabel>
</AddProjectWrapper>
);
};
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,130 @@
import React from 'react';
import styled from 'styled-components';
import Button from 'shared/components/Button';
export const ListActionsWrapper = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`;
export const ListActionItemWrapper = styled.li`
margin: 0;
padding: 0;
`;
export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #c2c6dc;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const ListSeparator = styled.hr`
background-color: #414561;
border: 0;
height: 1px;
margin: 8px 0;
padding: 0;
width: 100%;
`;
type Props = {
onDeleteProject: () => void;
};
const ProjectSettings: React.FC<Props> = ({ onDeleteProject }) => {
return (
<>
<ListActionsWrapper>
<ListActionItemWrapper onClick={() => onDeleteProject()}>
<ListActionItem>Delete Project</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
</>
);
};
type TeamSettingsProps = {
onDeleteTeam: () => void;
};
export const TeamSettings: React.FC<TeamSettingsProps> = ({ onDeleteTeam }) => {
return (
<>
<ListActionsWrapper>
<ListActionItemWrapper onClick={() => onDeleteTeam()}>
<ListActionItem>Delete Team</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
</>
);
};
const ConfirmWrapper = styled.div``;
const ConfirmSubTitle = styled.h3`
font-size: 14px;
`;
const ConfirmDescription = styled.div`
font-size: 14px;
`;
const DeleteList = styled.ul`
margin-bottom: 12px;
`;
const DeleteListItem = styled.li`
padding: 6px 0;
list-style: disc;
margin-left: 12px;
`;
const ConfirmDeleteButton = styled(Button)`
width: 100%;
padding: 6px 12px;
`;
type DeleteConfirmProps = {
description: string;
deletedItems: Array<string>;
onConfirmDelete: () => void;
};
export const DELETE_INFO = {
DELETE_PROJECTS: {
description: 'Deleting the project will also delete the following:',
deletedItems: ['Task groups and tasks'],
},
DELETE_TEAMS: {
description: 'Deleting the team will also delete the following:',
deletedItems: ['Projects under the team', 'All task groups & tasks associated with the team projects'],
},
};
const DeleteConfirm: React.FC<DeleteConfirmProps> = ({ description, deletedItems, onConfirmDelete }) => {
return (
<ConfirmWrapper>
<ConfirmDescription>
{description}
<DeleteList>
{deletedItems.map(item => (
<DeleteListItem>{item}</DeleteListItem>
))}
</DeleteList>
</ConfirmDescription>
<ConfirmDeleteButton onClick={() => onConfirmDelete()} color="danger">
Delete
</ConfirmDeleteButton>
</ConfirmWrapper>
);
};
export { DeleteConfirm };
export default ProjectSettings;

View File

@ -0,0 +1,106 @@
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: Array<TaskLabel> = [
{
id: 'development',
assignedDate: new Date().toString(),
projectLabel: {
id: 'development',
name: 'Development',
createdDate: 'date',
labelColor: {
id: 'label-color-blue',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
},
},
];
export const Default = () => {
const $cardRef: any = createRef();
const task: Task = {
id: 'task',
name: 'Hello, world!',
position: 1,
labels: labelData,
taskGroup: {
id: '1',
},
};
const [isEditorOpen, setEditorOpen] = useState(false);
const [target, setTarget] = useState<null | React.RefObject<HTMLElement>>(null);
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
return (
<>
{isEditorOpen && target && (
<QuickCardEditor
task={task}
onCloseEditor={() => setEditorOpen(false)}
onEditCard={action('edit card')}
onOpenLabelsPopup={action('open popup')}
onOpenDueDatePopup={action('open popup')}
onOpenMembersPopup={action('open popup')}
onToggleComplete={action('complete')}
onArchiveCard={action('archive card')}
target={target}
/>
)}
<List
id="1"
name="General"
isComposerOpen={false}
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
onExtraMenuOpen={(taskGroupID, $targetRef) => console.log(taskGroupID, $targetRef)}
>
<ListCards>
<Card
taskID="1"
taskGroupID="1"
description="hello!"
ref={$cardRef}
title={task.name}
onClick={action('on click')}
onContextMenu={e => {
setTarget($cardRef);
setEditorOpen(true);
}}
watched
labels={labelData.map(l => l.projectLabel)}
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,93 @@
import styled, { keyframes, css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.55);
bottom: 0;
color: #fff;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 100;
visibility: ${props => (props.open ? 'show' : 'hidden')};
`;
export const Container = styled.div<{ fixed: boolean; width: number; top: number; left: number }>`
position: absolute;
width: ${props => props.width}px;
top: ${props => props.top}px;
left: ${props => props.left}px;
${props =>
props.fixed &&
css`
top: auto;
bottom: ${props.top}px;
`}
`;
export const SaveButton = styled.button`
cursor: pointer;
background: rgb(115, 103, 240);
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<{ fixed: boolean }>`
left: 100%;
position: absolute;
top: 0;
width: 240px;
z-index: 0;
animation: ${FadeInAnimation} 85ms ease-in 1;
${props =>
props.fixed &&
css`
top: auto;
bottom: 8px;
`}
`;
export const EditorButton = styled.div`
cursor: pointer;
background: rgba(0, 0, 0, 0.6);
border-radius: 3px;
clear: both;
color: #c2c6dc;
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;
`;

View File

@ -0,0 +1,138 @@
import React, { useRef, useState } from 'react';
import Cross from 'shared/icons/Cross';
import styled from 'styled-components';
import { Wrapper, Container, EditorButtons, SaveButton, EditorButton, CloseButton } from './Styles';
import Card from '../Card';
export const CardMembers = styled.div`
position: absolute;
right: 0;
bottom: 0;
`;
type Props = {
task: Task;
onCloseEditor: () => void;
onEditCard: (taskGroupID: string, taskID: string, cardName: string) => void;
onToggleComplete: (task: Task) => void;
onOpenLabelsPopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onOpenMembersPopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onOpenDueDatePopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onArchiveCard: (taskGroupID: string, taskID: string) => void;
onCardMemberClick?: OnCardMemberClick;
target: React.RefObject<HTMLElement>;
};
const QuickCardEditor = ({
task,
onCloseEditor,
onOpenLabelsPopup,
onOpenMembersPopup,
onOpenDueDatePopup,
onToggleComplete,
onCardMemberClick,
onArchiveCard,
onEditCard,
target: $target,
}: Props) => {
const [currentCardTitle, setCardTitle] = useState(task.name);
const $labelsRef: any = useRef();
const $dueDate: any = useRef();
const $membersRef: any = useRef();
const handleCloseEditor = (e: any) => {
e.stopPropagation();
onCloseEditor();
};
const height = 180;
const saveCardButtonBarHeight = 48;
let top = 0;
let left = 0;
let width = 272;
let fixed = false;
if ($target && $target.current) {
const pos = $target.current.getBoundingClientRect();
top = pos.top;
left = pos.left;
width = pos.width;
const isFixed = window.innerHeight / 2 < pos.top;
if (isFixed) {
top = window.innerHeight - pos.bottom - saveCardButtonBarHeight;
fixed = true;
}
}
return (
<Wrapper onClick={handleCloseEditor} open>
<CloseButton onClick={handleCloseEditor}>
<Cross width={16} height={16} />
</CloseButton>
<Container fixed={fixed} width={width} left={left} top={top}>
<Card
editable
onCardMemberClick={onCardMemberClick}
title={currentCardTitle}
onEditCard={(taskGroupID, taskID, name) => {
onEditCard(taskGroupID, taskID, name);
onCloseEditor();
}}
complete={task.complete ?? false}
members={task.assigned}
taskID={task.id}
taskGroupID={task.taskGroup.id}
labels={task.labels.map(l => l.projectLabel)}
/>
<SaveButton onClick={() => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton>
<EditorButtons fixed={fixed}>
<EditorButton
onClick={e => {
e.stopPropagation();
onToggleComplete(task);
}}
>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</EditorButton>
<EditorButton
ref={$membersRef}
onClick={e => {
e.stopPropagation();
onOpenMembersPopup($membersRef, task);
}}
>
Edit Assigned
</EditorButton>
<EditorButton
ref={$dueDate}
onClick={e => {
e.stopPropagation();
onOpenDueDatePopup($labelsRef, task);
}}
>
Edit Due Date
</EditorButton>
<EditorButton
ref={$labelsRef}
onClick={e => {
e.stopPropagation();
onOpenLabelsPopup($labelsRef, task);
}}
>
Edit Labels
</EditorButton>
<EditorButton
onClick={e => {
e.stopPropagation();
onArchiveCard(task.taskGroup.id, task.id);
onCloseEditor();
}}
>
Archive
</EditorButton>
</EditorButtons>
</Container>
</Wrapper>
);
};
export default QuickCardEditor;

View File

@ -0,0 +1,126 @@
import React from 'react';
import Select from 'react-select';
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
const colourStyles = {
control: (styles: any, data: any) => {
return {
...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
},
':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)',
},
};
},
menu: (styles: any) => {
return {
...styles,
backgroundColor: mixin.darken('#262c49', 0.15),
};
},
dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
indicatorSeparator: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
option: (styles: any, { data, isDisabled, isFocused, isSelected }: any) => {
return {
...styles,
backgroundColor: isDisabled
? null
: isSelected
? mixin.darken('#262c49', 0.25)
: isFocused
? mixin.darken('#262c49', 0.15)
: null,
color: isDisabled ? '#ccc' : isSelected ? '#fff' : '#c2c6dc',
cursor: isDisabled ? 'not-allowed' : 'default',
':active': {
...styles[':active'],
backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'),
},
':hover': {
...styles[':hover'],
backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'),
},
};
},
placeholder: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
clearIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
input: (styles: any) => ({
...styles,
color: '#fff',
}),
singleValue: (styles: any) => {
return {
...styles,
color: '#fff',
};
},
};
const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width};
padding-left: 0.7rem;
color: rgba(115, 103, 240);
left: 0;
top: 0;
transition: all 0.2s ease;
position: absolute;
border-radius: 5px;
overflow: hidden;
font-size: 0.85rem;
cursor: text;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const SelectContainer = styled.div`
position: relative;
padding-top: 24px;
`;
type SelectProps = {
label?: string;
onChange: (e: any) => void;
value: any;
options: Array<any>;
};
const SelectElement: React.FC<SelectProps> = ({ onChange, value, options, label }) => {
return (
<SelectContainer>
<Select
onChange={(e: any) => {
onChange(e);
}}
value={value}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}
/>
{label && <InputLabel width="100%">{label}</InputLabel>}
</SelectContainer>
);
};
export default SelectElement;

View File

@ -0,0 +1,35 @@
import React, { createRef, useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Settings from '.';
export default {
component: Settings,
title: 'Settings',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const profile = {
id: '1',
fullName: 'Jordan Knott',
username: 'jordanthedev',
profileIcon: { url: 'http://localhost:3333/uploads/headshot.png', bgColor: '#000', initials: 'JK' },
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Settings
profile={profile}
onProfileAvatarRemove={action('remove')}
onProfileAvatarChange={action('profile avatar change')}
/>
</>
);
};

View File

@ -0,0 +1,284 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User } from 'shared/icons';
import Input from 'shared/components/Input';
import Button from 'shared/components/Button';
const ProfileContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 2.2rem !important;
`;
const AvatarContainer = styled.div`
width: 70px;
height: 70px;
border-radius: 50%;
position: relative;
cursor: pointer;
display: inline-block;
margin: 5px;
margin-bottom: 1rem;
margin-right: 1rem;
`;
const AvatarMask = styled.div<{ background: string }>`
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: ${props => props.background};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
`;
const AvatarImg = styled.img<{ src: string }>`
display: block;
width: 100%;
height: 100%;
`;
const ActionButtons = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`;
const UploadButton = styled(Button)`
margin-right: 1rem;
display: inline-block;
`;
const RemoveButton = styled(Button)`
display: inline-block;
`;
const ImgLabel = styled.p`
color: #c2c6dc;
margin-top: 0.5rem;
font-size: 12.25px;
width: 100%;
`;
const AvatarInitials = styled.span`
font-size: 32px;
color: #fff;
`;
type AvatarSettingsProps = {
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
profile: ProfileIcon;
};
const AvatarSettings: React.FC<AvatarSettingsProps> = ({ profile, onProfileAvatarChange, onProfileAvatarRemove }) => {
return (
<ProfileContainer>
<AvatarContainer>
<AvatarMask
background={profile.url ? 'none' : profile.bgColor ?? 'none'}
onClick={() => onProfileAvatarChange()}
>
{profile.url ? (
<AvatarImg alt="" src={profile.url ?? ''} />
) : (
<AvatarInitials>{profile.initials}</AvatarInitials>
)}
</AvatarMask>
</AvatarContainer>
<ActionButtons>
<UploadButton onClick={() => onProfileAvatarChange()}>Upload photo</UploadButton>
<RemoveButton variant="outline" color="danger" onClick={() => onProfileAvatarRemove()}>
Remove
</RemoveButton>
<ImgLabel>Allowed JPG, GIF or PNG. Max size of 800kB</ImgLabel>
</ActionButtons>
</ProfileContainer>
);
};
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
position: relative;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover {
color: rgba(115, 103, 240);
}
&:hover svg {
fill: rgba(115, 103, 240);
}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
margin-left: 1rem;
position: relative;
display: block;
overflow: hidden;
width: 100%;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
border-radius: 0.5rem;
`;
const TabContentInner = styled.div``;
const items = [{ name: 'General' }, { name: 'Change Password' }, { name: 'Info' }, { name: 'Notifications' }];
type NavItemProps = {
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
};
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
};
const SettingActions = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
`;
const SaveButton = styled(Button)`
margin-right: 1rem;
display: inline-block;
`;
type SettingsProps = {
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
profile: TaskUser;
};
const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAvatarChange, profile }) => {
const [currentTab, setTab] = useState(0);
const [currentTop, setTop] = useState(0);
const $tabNav = useRef<HTMLDivElement>(null);
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<AvatarSettings
onProfileAvatarRemove={onProfileAvatarRemove}
onProfileAvatarChange={onProfileAvatarChange}
profile={profile.profileIcon}
/>
<Input value={profile.fullName} width="100%" label="Name" />
<Input
value={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
width="100%"
label="Initials "
/>
<Input value={profile.username ?? ''} width="100%" label="Username " />
<Input width="100%" label="Email" />
<Input width="100%" label="Bio" />
<SettingActions>
<SaveButton>Save Change</SaveButton>
</SettingActions>
</TabContent>
</TabContentWrapper>
</Container>
);
};
export default Settings;

View File

@ -0,0 +1,27 @@
import React from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Navbar from 'shared/components/Navbar';
import Sidebar from '.';
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,17 @@
import styled from 'styled-components';
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);
`;
export default Container;

View File

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

View File

@ -0,0 +1,25 @@
import React, { useRef } from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Tabs from '.';
export default {
component: Tabs,
title: 'Tabs',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Tabs />
</>
);
};

View File

@ -0,0 +1,8 @@
import React from 'react';
import styled from 'styled-components';
const Tabs = () => {
return <span>HEllo!</span>;
};
export default Tabs;

View File

@ -0,0 +1,76 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
import { DoubleChevronUp, Crown } from 'shared/icons';
export const AdminIcon = styled(DoubleChevronUp)`
bottom: 0;
right: 1px;
position: absolute;
fill: #c377e0;
`;
export const OwnerIcon = styled(Crown)`
bottom: 0;
right: 1px;
position: absolute;
fill: #c8b928;
`;
const TaskDetailAssignee = styled.div`
cursor: pointer;
margin: 0 0 0 -2px;
border-radius: 50%;
display: flex;
align-items: center;
position: relative;
float: left;
`;
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(${props => (props.backgroundURL ? props.theme.colors.text.primary : '0,0,0')});
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
font-size: 14px;
font-weight: 400;
&:hover {
opacity: 0.8;
}
`;
type TaskAssigneeProps = {
size: number | string;
showRoleIcons?: boolean;
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
className?: string;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ showRoleIcons, member, onMemberProfile, size, className }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee
className={className}
ref={$memberRef}
onClick={e => {
e.stopPropagation();
onMemberProfile($memberRef, member.id);
}}
key={member.id}
>
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
</Wrapper>
{showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />}
{showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />}
</TaskDetailAssignee>
);
};
export default TaskAssignee;

View File

@ -0,0 +1,391 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
export const TaskHeader = styled.div`
padding: 21px 30px 0px;
margin-right: 70px;
display: flex;
flex-direction: column;
`;
export const TaskMeta = styled.div`
position: relative;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
border-radius: 4px;
`;
export const TaskGroupLabel = styled.span`
color: #c2c6dc;
font-size: 14px;
`;
export const TaskGroupLabelName = styled.span`
color: #c2c6dc;
text-decoration: underline;
font-size: 14px;
`;
export const TaskActions = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 21px 18px 0px;
display: flex;
align-items: center;
`;
export const TaskAction = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
cursor: pointer;
padding: 0px 9px;
`;
export const TaskDetailsWrapper = styled.div`
display: flex;
padding: 0px 16px 60px;
`;
export const TaskDetailsContent = styled.div`
flex: 1;
padding-right: 8px;
`;
export const TaskDetailsSidebar = styled.div`
width: 168px;
padding-left: 8px;
`;
export const TaskDetailsTitleWrapper = styled.div`
height: 44px;
width: 100%;
margin: 0 0 0 -8px;
display: inline-block;
`;
export const TaskDetailsTitle = styled(TextareaAutosize)`
line-height: 1.28;
resize: none;
box-shadow: transparent 0px 0px 0px 1px;
font-size: 24px;
font-family: 'Droid Sans';
font-weight: 700;
padding: 4px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
transition: background 0.1s ease 0s;
overflow-y: hidden;
width: 100%;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const TaskDetailsLabel = styled.div`
padding: 24px 0px 12px;
font-size: 15px;
font-weight: 600;
color: #c2c6dc;
`;
export const TaskDetailsAddDetailsButton = styled.div`
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: none;
border: none;
border-radius: 3px;
display: block;
min-height: 56px;
padding: 8px 12px;
text-decoration: none;
font-size: 14px;
cursor: pointer;
color: #c2c6dc;
&:hover {
background: ${mixin.darken('#262c49', 0.25)};
box-shadow: none;
border: none;
}
`;
export const TaskDetailsEditorWrapper = styled.div`
display: block;
float: left;
padding-bottom: 9px;
z-index: 50;
width: 100%;
`;
export const TaskDetailsEditor = styled(TextareaAutosize)`
width: 100%;
min-height: 108px;
color: #c2c6dc;
background: #262c49;
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
outline: none;
border: none;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.05)};
}
`;
export const TaskDetailsMarkdown = styled.div`
width: 100%;
cursor: pointer;
color: #c2c6dc;
h1 {
font-size: 24px;
font-weight: 600;
line-height: 28px;
margin: 0 0 12px;
}
h2 {
font-weight: 600;
font-size: 20px;
line-height: 24px;
margin: 16px 0 8px;
}
p {
margin: 0 0 8px;
}
strong {
font-weight: 700;
}
ul {
margin: 8px 0;
}
ul > li {
margin: 8px 8px 8px 24px;
list-style: disc;
}
p a {
color: rgba(${props => props.theme.colors.primary});
}
`;
export const TaskDetailsControls = styled.div`
clear: both;
margin-top: 8px;
display: flex;
`;
export const ConfirmSave = styled(Button)`
padding: 6px 12px;
border-radius: 3px;
margin-right: 6px;
`;
export const CancelEdit = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;
export const TaskDetailSectionTitle = styled.div`
text-transform: uppercase;
color: #c2c6dc;
font-size: 12.5px;
font-weight: 600;
margin: 24px 0px 5px;
`;
export const TaskDetailAssignees = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`;
export const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
`;
export const TaskDetailsAddMemberIcon = styled.div`
float: left;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const TaskDetailLabels = styled.div`
display: flex;
align-items: center;
flex-wrap: wrap;
`;
export const TaskDetailLabel = styled.div<{ color: string }>`
&:hover {
opacity: 0.8;
}
background-color: ${props => props.color};
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 3px;
box-sizing: border-box;
display: block;
float: left;
font-weight: 600;
height: 32px;
line-height: 32px;
margin: 0 4px 4px 0;
min-width: 40px;
padding: 0 12px;
width: auto;
`;
export const TaskDetailsAddLabel = styled.div`
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const TaskDetailsAddLabelIcon = styled.div`
float: left;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const NoDueDateLabel = styled.span`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
`;
export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
`;
export const ActionButtons = styled.div`
display: flex;
flex-direction: column;
`;
export const ActionButtonsTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
`;
export const ActionButton = styled(Button)`
margin-top: 8px;
padding: 6px 12px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
text-align: left;
&:hover {
box-shadow: none;
background: rgba(${props => props.theme.colors.bg.primary}, 0.6);
}
`;
export const MetaDetails = styled.div`
margin-top: 8px;
display: flex;
`;
export const TaskDueDateButton = styled(Button)`
height: 32px;
padding: 6px 12px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
&:hover {
box-shadow: none;
background: rgba(${props => props.theme.colors.bg.primary}, 0.6);
}
`;
export const MetaDetail = styled.div`
display: block;
float: left;
margin: 0 16px 8px 0;
max-width: 100%;
`;
export const TaskDetailsSection = styled.div`
display: block;
`;
export const MetaDetailTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
margin-top: 16px;
text-transform: uppercase;
display: block;
line-height: 20px;
margin: 0 8px 4px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
export const MetaDetailContent = styled.div``;

View File

@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Modal from 'shared/components/Modal';
import TaskDetails from '.';
export default {
component: TaskDetails,
title: 'TaskDetails',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
const [description, setDescription] = useState('');
return (
<>
<NormalizeStyles />
<BaseStyles />
<Modal
width={1040}
onClose={action('on close')}
renderContent={() => {
return (
<TaskDetails
onDeleteItem={action('delete item')}
onChangeItemName={action('change item name')}
task={{
id: '1',
taskGroup: { name: 'General', id: '1' },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description,
assigned: [
{
id: '1',
profileIcon: { bgColor: null, url: null, initials: null },
fullName: 'Jordan Knott',
},
],
}}
onTaskNameChange={action('task name change')}
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onAddItem={action('add item')}
onToggleTaskComplete={action('toggle task complete')}
onToggleChecklistItem={action('toggle checklist item')}
onOpenAddLabelPopup={action('open add label popup')}
onChangeChecklistName={action('change checklist name')}
onDeleteChecklist={action('delete checklist')}
onOpenAddChecklistPopup={action(' open checklist')}
onOpenDueDatePopop={action('open due date popup')}
/>
);
}}
/>
</>
);
};

View File

@ -0,0 +1,340 @@
import React, { useState, useRef, useEffect } from 'react';
import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import ReactMarkdown from 'react-markdown';
import TaskAssignee from 'shared/components/TaskAssignee';
import moment from 'moment';
import {
NoDueDateLabel,
TaskDueDateButton,
UnassignedLabel,
TaskGroupLabel,
TaskGroupLabelName,
TaskDetailsSection,
TaskActions,
TaskDetailsAddLabel,
TaskDetailsAddLabelIcon,
TaskAction,
TaskMeta,
ActionButtons,
ActionButton,
ActionButtonsTitle,
TaskHeader,
ProfileIcon,
TaskDetailsContent,
TaskDetailsWrapper,
TaskDetailsSidebar,
TaskDetailsTitleWrapper,
TaskDetailsTitle,
TaskDetailsLabel,
TaskDetailsAddDetailsButton,
TaskDetailsEditor,
TaskDetailsEditorWrapper,
TaskDetailsMarkdown,
TaskDetailsControls,
ConfirmSave,
CancelEdit,
TaskDetailSectionTitle,
TaskDetailLabel,
TaskDetailLabels,
TaskDetailAssignee,
TaskDetailAssignees,
TaskDetailsAddMemberIcon,
MetaDetails,
MetaDetail,
MetaDetailTitle,
MetaDetailContent,
} from './Styles';
import Checklist from '../Checklist';
type TaskContentProps = {
onEditContent: () => void;
description: string;
};
type TaskLabelProps = {
label: TaskLabel;
onClick: ($target: React.RefObject<HTMLElement>) => void;
};
const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => {
const $label = useRef<HTMLDivElement>(null);
return (
<TaskDetailLabel
onClick={() => {
onClick($label);
}}
ref={$label}
color={label.projectLabel.labelColor.colorHex}
>
{label.projectLabel.name}
</TaskDetailLabel>
);
};
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
) : (
<TaskDetailsMarkdown onClick={onEditContent}>
<ReactMarkdown source={description} />
</TaskDetailsMarkdown>
);
};
type DetailsEditorProps = {
description: string;
onTaskDescriptionChange: (newDescription: string) => void;
onCancel: () => void;
};
const DetailsEditor: React.FC<DetailsEditorProps> = ({
description: initialDescription,
onTaskDescriptionChange,
onCancel,
}) => {
const [description, setDescription] = useState(initialDescription);
const $editorWrapperRef = useRef<HTMLDivElement>(null);
const $editorRef = useRef<HTMLTextAreaElement>(null);
const handleOutsideClick = () => {
onTaskDescriptionChange(description);
};
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
$editorRef.current.select();
}
}, []);
useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null);
return (
<TaskDetailsEditorWrapper ref={$editorWrapperRef}>
<TaskDetailsEditor
ref={$editorRef}
value={description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
/>
<TaskDetailsControls>
<ConfirmSave variant="relief" onClick={handleOutsideClick}>
Save
</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Plus size={16} color="#c2c6dc" />
</CancelEdit>
</TaskDetailsControls>
</TaskDetailsEditorWrapper>
);
};
type TaskDetailsProps = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onAddItem: (checklistID: string, name: string, position: number) => void;
onDeleteItem: (itemID: string) => void;
onChangeItemName: (itemID: string, itemName: string) => void;
onToggleTaskComplete: (task: Task) => void;
onToggleChecklistItem: (itemID: string, complete: boolean) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onChangeChecklistName: (checklistID: string, name: string) => void;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
onCloseModal: () => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({
task,
onDeleteChecklist,
onTaskNameChange,
onOpenAddChecklistPopup,
onChangeChecklistName,
onToggleTaskComplete,
onTaskDescriptionChange,
onChangeItemName,
onDeleteItem,
onDeleteTask,
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onOpenDueDatePopop,
onAddItem,
onToggleChecklistItem,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
const [taskName, setTaskName] = useState(task.name);
const handleClick = () => {
setEditorOpen(!editorOpen);
};
const handleDeleteTask = () => {
onDeleteTask(task);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
onTaskNameChange(task, taskName);
}
};
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = ($target: React.RefObject<HTMLElement>) => {
onOpenAddMemberPopup(task, $target);
};
const onAddChecklist = ($target: React.RefObject<HTMLElement>) => {
onOpenAddChecklistPopup(task, $target)
}
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
onOpenAddLabelPopup(task, $target);
};
return (
<>
<TaskActions>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} color="#c2c6dc" />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross width={16} height={16} />
</TaskAction>
</TaskActions>
<TaskHeader>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle
value={taskName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setTaskName(e.currentTarget.value)}
onKeyDown={onKeyDown}
/>
</TaskDetailsTitleWrapper>
<TaskMeta>
{task.taskGroup.name && (
<TaskGroupLabel>
in list <TaskGroupLabelName>{task.taskGroup.name}</TaskGroupLabelName>
</TaskGroupLabel>
)}
</TaskMeta>
</TaskHeader>
<TaskDetailsWrapper>
<TaskDetailsContent>
<MetaDetails>
{task.assigned && task.assigned.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>MEMBERS</MetaDetailTitle>
<MetaDetailContent>
{task.assigned &&
task.assigned.map(member => (
<TaskAssignee key={member.id} size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMemberIcon ref={$addMemberRef} onClick={() => onAddMember($addMemberRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.labels.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>LABELS</MetaDetailTitle>
<MetaDetailContent>
{task.labels.map(label => {
return (
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabelIcon ref={$addLabelRef} onClick={() => onAddLabel($addLabelRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddLabelIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.dueDate && (
<MetaDetail>
<MetaDetailTitle>DUE DATE</MetaDetailTitle>
<MetaDetailContent>
<TaskDueDateButton>{moment(task.dueDate).format('MMM D [at] h:mm A')}</TaskDueDateButton>
</MetaDetailContent>
</MetaDetail>
)}
</MetaDetails>
<TaskDetailsSection>
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
description={description}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
setDescription(newDescription);
onTaskDescriptionChange(task, newDescription);
}}
onCancel={() => {
setEditorOpen(false);
}}
/>
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map(checklist => (
<Checklist
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={onDeleteChecklist}
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
onToggleItem={onToggleChecklistItem}
onDeleteItem={onDeleteItem}
onAddItem={n => {
if (task.checklists) {
let position = 65535;
const [lastItem] = checklist.items.sort((a, b) => a.position - b.position).slice(-1);
if (lastItem) {
position = lastItem.position * 2 + 1;
}
onAddItem(checklist.id, n, position);
}
}}
onChangeItemName={onChangeItemName}
/>
))}
</TaskDetailsSection>
</TaskDetailsContent>
<TaskDetailsSidebar>
<ActionButtons>
<ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle>
<ActionButton onClick={() => onToggleTaskComplete(task)}>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</ActionButton>
<ActionButton onClick={$target => onAddMember($target)}>Members</ActionButton>
<ActionButton onClick={$target => onAddLabel($target)}>Labels</ActionButton>
<ActionButton onClick={$target => onAddChecklist($target)}>Checklist</ActionButton>
<ActionButton onClick={$target => onOpenDueDatePopop(task, $target)}>Due Date</ActionButton>
<ActionButton>Attachment</ActionButton>
<ActionButton>Cover</ActionButton>
</ActionButtons>
</TaskDetailsSidebar>
</TaskDetailsWrapper>
</>
);
};
export default TaskDetails;

View File

@ -0,0 +1,31 @@
import styled from 'styled-components/macro';
import TextareaAutosize from 'react-autosize-textarea';
const Textarea = styled(TextareaAutosize)`
border: none;
resize: none;
overflow: hidden;
overflow-wrap: break-word;
background: transparent;
border-radius: 3px;
box-shadow: none;
margin: -4px 0;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
flex-direction: column;
text-align: start;
color: #c2c6dc;
font-weight: 600;
font-size: 20px;
padding: 3px 10px 3px 8px;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
}
`;
export default Textarea;

View File

@ -0,0 +1,263 @@
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
import { Citadel } from 'shared/icons';
import { NavLink, Link } from 'react-router-dom';
import TaskAssignee from 'shared/components/TaskAssignee';
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${props => props.zIndex};
position: relative;
`;
export const NavbarWrapper = styled.div`
width: 100%;
`;
export const ProjectMembers = styled.div`
display: flex;
align-items: center;
`;
export const NavbarHeader = styled.header`
height: 80px;
padding: 0 1.75rem;
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);
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;
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`
flex: 1;
align-items: flex-start;
display: flex;
flex-direction: column;
min-width: 1px;
`;
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 IconContainer = styled.div<{ disabled?: boolean }>`
margin-right: 20px;
cursor: pointer;
${props =>
props.disabled &&
css`
opacity: 0.5;
cursor: default;
pointer-events: none;
`}
`;
export const ProfileNamePrimary = styled.div`
color: #c2c6dc;
font-weight: 600;
`;
export const ProfileNameSecondary = styled.small`
color: #c2c6dc;
`;
export const ProfileIcon = styled.div<{ bgColor: string | null; backgroundURL: string | null }>`
width: 40px;
height: 40px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
display: flex;
${props => !props.nameOnly && 'padding-top: 9px;'}
margin-left: -14px;
align-items: center;
max-width: 100%;
min-height: 51px;
`;
export const ProjectTabs = styled.div`
align-items: flex-end;
align-self: stretch;
display: flex;
flex: 1 0 auto;
justify-content: flex-start;
max-width: 100%;
`;
export const ProjectTab = styled(NavLink)`
font-size: 80%;
color: rgba(${props => props.theme.colors.text.primary});
font-size: 15px;
cursor: pointer;
display: flex;
line-height: normal;
min-width: 1px;
transition-duration: 0.2s;
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding-bottom: 12px;
&:not(:last-child) {
margin-right: 20px;
}
&:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary});
color: rgba(${props => props.theme.colors.text.secondary});
}
&.active {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
}
&.active:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
}
`;
export const ProjectName = styled.h1`
color: rgba(${props => props.theme.colors.text.primary});
font-weight: 600;
font-size: 20px;
padding: 3px 10px 3px 8px;
margin: -4px 0;
`;
export const ProjectNameTextarea = styled(TextareaAutosize)`
border: none;
resize: none;
overflow: hidden;
overflow-wrap: break-word;
background: transparent;
border-radius: 3px;
box-shadow: none;
margin: -4px 0;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
flex-direction: column;
text-align: start;
color: #c2c6dc;
font-weight: 600;
font-size: 20px;
padding: 3px 10px 3px 8px;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
}
`;
export const ProjectSwitcher = styled.button`
font-size: 20px;
outline: none;
border: none;
width: 100px;
border-radius: 3px;
line-height: 20px;
padding: 6px 4px;
background-color: none;
text-align: center;
color: #c2c6dc;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const Separator = styled.div`
color: #c2c6dc;
font-size: 20px;
padding-left: 4px;
padding-right: 4px;
`;
export const ProjectSettingsButton = styled.button`
outline: none;
border: none;
border-radius: 3px;
line-height: 20px;
width: 28px;
height: 28px;
background-color: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const InviteButton = styled(Button)`
margin: 0 0 0 8px;
padding: 6px 12px;
`;
export const ProjectFinder = styled(Button)`
margin-right: 20px;
padding: 6px 12px;
`;
export const NavSeparator = styled.div`
width: 1px;
background: rgba(${props => props.theme.colors.border});
height: 34px;
margin: 0 20px;
`;
export const LogoContainer = styled(Link)`
display: block;
left: 50%;
position: absolute;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
`;
export const CitadelTitle = styled.h2`
margin-left: 5px;
color: rgba(${props => props.theme.colors.text.primary});
font-size: 20px;
`;
export const CitadelLogo = styled(Citadel)`
fill: rgba(${props => props.theme.colors.text.primary});
stroke: rgba(${props => props.theme.colors.text.primary});
`;

View File

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import { action } from '@storybook/addon-actions';
import DropdownMenu from 'shared/components/DropdownMenu';
import TopNavbar from '.';
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 = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<TopNavbar
onOpenProjectFinder={action('finder')}
name="Projects"
user={{
id: '1',
fullName: 'Jordan Knott',
profileIcon: {
url: null,
initials: 'JK',
bgColor: '#000',
},
}}
onChangeRole={action('change role')}
onNotificationClick={action('notifications click')}
onOpenSettings={action('open settings')}
onDashboardClick={action('open dashboard')}
onRemoveFromBoard={action('remove project')}
onProfileClick={action('profile click')}
/>
</>
);
};

View File

@ -0,0 +1,312 @@
import React, { useRef, useState, useEffect } from 'react';
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
import styled from 'styled-components';
import ProfileIcon from 'shared/components/ProfileIcon';
import TaskAssignee from 'shared/components/TaskAssignee';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql';
import MiniProfile from 'shared/components/MiniProfile';
import {
CitadelLogo,
CitadelTitle,
ProjectFinder,
LogoContainer,
NavSeparator,
IconContainer,
ProjectNameTextarea,
InviteButton,
GlobalActions,
ProjectActions,
ProjectMeta,
ProjectName,
ProjectTabs,
ProjectTab,
NavbarWrapper,
NavbarHeader,
ProjectSettingsButton,
ProfileContainer,
ProfileNameWrapper,
ProfileNamePrimary,
ProfileNameSecondary,
ProjectMember,
ProjectMembers,
} from './Styles';
import { Link } from 'react-router-dom';
const HomeDashboard = styled(Home)``;
type ProjectHeadingProps = {
onFavorite?: () => void;
name: string;
onSaveProjectName?: (projectName: string) => void;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
};
const ProjectHeading: React.FC<ProjectHeadingProps> = ({
onFavorite,
name: initialProjectName,
onSaveProjectName,
onOpenSettings,
}) => {
const [isEditProjectName, setEditProjectName] = useState(false);
const [projectName, setProjectName] = useState(initialProjectName);
const $projectName = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isEditProjectName && $projectName && $projectName.current) {
$projectName.current.focus();
$projectName.current.select();
}
}, [isEditProjectName]);
useEffect(() => {
setProjectName(initialProjectName);
}, [initialProjectName]);
const onProjectNameChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
setProjectName(event.currentTarget.value);
};
const onProjectNameBlur = () => {
if (onSaveProjectName) {
onSaveProjectName(projectName);
}
setEditProjectName(false);
};
const onProjectNameKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
if ($projectName && $projectName.current) {
$projectName.current.blur();
}
}
};
const $settings = useRef<HTMLButtonElement>(null);
return (
<>
{isEditProjectName ? (
<ProjectNameTextarea
ref={$projectName}
onChange={onProjectNameChange}
onKeyDown={onProjectNameKeyDown}
onBlur={onProjectNameBlur}
spellCheck={false}
value={projectName}
/>
) : (
<ProjectName
onClick={() => {
setEditProjectName(true);
}}
>
{projectName}
</ProjectName>
)}
<ProjectSettingsButton
onClick={() => {
onOpenSettings($settings);
}}
ref={$settings}
>
<AngleDown color="#c2c6dc" />
</ProjectSettingsButton>
{onFavorite && (
<ProjectSettingsButton onClick={() => onFavorite()}>
<Star width={16} height={16} color="#c2c6dc" />
</ProjectSettingsButton>
)}
</>
);
};
export type MenuItem = {
name: string;
link: string;
};
type MenuTypes = {
[key: string]: Array<string>;
};
export const MENU_TYPES: MenuTypes = {
PROJECT_MENU: ['Board', 'Timeline', 'Calender'],
TEAM_MENU: ['Projects', 'Members', 'Settings'],
};
type NavBarProps = {
menuType?: Array<MenuItem> | null;
name: string | null;
currentTab?: number;
onSetTab?: (tab: number) => void;
onOpenProjectFinder: ($target: React.RefObject<HTMLElement>) => void;
onChangeProjectOwner?: (userID: string) => void;
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
onFavorite?: () => void;
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
onSaveName?: (name: string) => void;
onNotificationClick: () => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onDashboardClick: () => void;
user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
projectMembers?: Array<TaskUser> | null;
onRemoveFromBoard?: (userID: string) => void;
};
const NavBar: React.FC<NavBarProps> = ({
menuType,
onInviteUser,
onChangeProjectOwner,
currentTab,
onOpenProjectFinder,
onFavorite,
onSetTab,
onChangeRole,
name,
onRemoveFromBoard,
onSaveName,
onProfileClick,
onNotificationClick,
onDashboardClick,
user,
projectMembers,
onOpenSettings,
}) => {
const handleProfileClick = ($target: React.RefObject<HTMLElement>) => {
if ($target && $target.current) {
onProfileClick($target);
}
};
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
if (member) {
console.log(member);
showPopup(
$targetRef,
<MiniProfile
warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeProjectOwner={
member.role && member.role.code !== 'owner'
? (userID: string) => {
if (user && onChangeProjectOwner) {
onChangeProjectOwner(userID);
}
}
: undefined
}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
if (onChangeRole) {
onChangeRole(member.id, roleCode);
}
}}
onRemoveFromBoard={
member.role && member.role.code === 'owner'
? undefined
: () => {
if (onRemoveFromBoard) {
onRemoveFromBoard(member.id);
}
}
}
user={member}
bio=""
/>,
);
}
};
return (
<NavbarWrapper>
<NavbarHeader>
<ProjectActions>
<ProjectMeta>
{name && (
<ProjectHeading
onFavorite={onFavorite}
onOpenSettings={onOpenSettings}
name={name}
onSaveProjectName={onSaveName}
/>
)}
</ProjectMeta>
{name && (
<ProjectTabs>
{menuType &&
menuType.map((menu, idx) => {
return (
<ProjectTab
key={menu.name}
to={menu.link}
exact
onClick={() => {
// TODO
}}
>
{menu.name}
</ProjectTab>
);
})}
</ProjectTabs>
)}
</ProjectActions>
<LogoContainer to="/">
<CitadelLogo width={24} height={24} />
<CitadelTitle>Citadel</CitadelTitle>
</LogoContainer>
<GlobalActions>
{projectMembers && (
<>
<ProjectMembers>
{projectMembers.map((member, idx) => (
<ProjectMember
showRoleIcons
zIndex={projectMembers.length - idx}
key={member.id}
size={28}
member={member}
onMemberProfile={onMemberProfile}
/>
))}
<InviteButton
onClick={$target => {
if (onInviteUser) {
onInviteUser($target);
}
}}
variant="outline"
>
Invite
</InviteButton>
</ProjectMembers>
<NavSeparator />
</>
)}
<ProjectFinder onClick={onOpenProjectFinder} variant="gradient">
Projects
</ProjectFinder>
<IconContainer onClick={onDashboardClick}>
<HomeDashboard width={20} height={20} />
</IconContainer>
<IconContainer disabled>
<CheckCircle width={20} height={20} />
</IconContainer>
<IconContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
</IconContainer>
<IconContainer disabled>
<BarChart width={20} height={20} />
</IconContainer>
{user && (
<IconContainer>
<ProfileIcon user={user} size={30} onProfileClick={handleProfileClick} />
</IconContainer>
)}
</GlobalActions>
</NavbarHeader>
</NavbarWrapper>
);
};
export default NavBar;