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