diff --git a/web/package.json b/web/package.json index 548db58..ca93c15 100644 --- a/web/package.json +++ b/web/package.json @@ -58,7 +58,8 @@ "eject": "react-scripts eject", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public", - "generate": "graphql-codegen" + "generate": "graphql-codegen", + "lint": "eslint --ext js,ts,tsx src" }, "eslintConfig": { "extends": "react-app" diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index b838401..e84a5da 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -11,6 +11,7 @@ type Task = { name: string; position: number; labels: Label[]; + description?: string; }; type TaskGroup = { diff --git a/web/src/shared/components/Modal/Modal.stories.tsx b/web/src/shared/components/Modal/Modal.stories.tsx new file mode 100644 index 0000000..5a7eeb2 --- /dev/null +++ b/web/src/shared/components/Modal/Modal.stories.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import NormalizeStyles from 'App/NormalizeStyles'; +import BaseStyles from 'App/BaseStyles'; +import styled from 'styled-components'; +import Modal from './index'; + +export default { + component: Modal, + title: 'Modal', + parameters: { + backgrounds: [ + { name: 'white', value: '#ffffff' }, + { name: 'gray', value: '#cdd3e1', default: true }, + ], + }, +}; + +export const Default = () => { + return ( + <> + + + { + return

Hello!

; + }} + /> + + ); +}; diff --git a/web/src/shared/components/Modal/Styles.ts b/web/src/shared/components/Modal/Styles.ts new file mode 100644 index 0000000..b6ec642 --- /dev/null +++ b/web/src/shared/components/Modal/Styles.ts @@ -0,0 +1,33 @@ +import styled from 'styled-components'; +import { mixin } from 'shared/utils/styles'; + +export const ScrollOverlay = styled.div` + z-index: 1000000; + 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(9, 30, 66, 0.54); + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +`; + +export const StyledModal = styled.div<{ width: number }>` + display: inline-block; + position: relative; + width: 100%; + background: #fff; + max-width: ${props => props.width}px; + vertical-align: middle; + border-radius: 3px; + ${mixin.boxShadowMedium} +`; diff --git a/web/src/shared/components/Modal/index.tsx b/web/src/shared/components/Modal/index.tsx new file mode 100644 index 0000000..70b4e69 --- /dev/null +++ b/web/src/shared/components/Modal/index.tsx @@ -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')!; + +type ModalProps = { + width: number; + onClose: () => void; + renderContent: () => JSX.Element; +}; + +const Modal: React.FC = ({ width, onClose: tellParentToClose, renderContent }) => { + const $modalRef = useRef(null); + const $clickableOverlayRef = useRef(null); + + useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef); + useOnEscapeKeyDown(true, tellParentToClose); + + return ReactDOM.createPortal( + + + + {renderContent()} + + + , + $root, + ); +}; + +export default Modal; diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts new file mode 100644 index 0000000..e5ae0c9 --- /dev/null +++ b/web/src/shared/components/TaskDetails/Styles.ts @@ -0,0 +1,181 @@ +import styled from 'styled-components'; +import TextareaAutosize from 'react-autosize-textarea/lib'; + +export const TaskHeader = styled.div` + display: flex; + -webkit-box-pack: justify; + justify-content: space-between; + padding: 21px 18px 0px; +`; + +export const TaskMeta = styled.div` + position: relative; + cursor: pointer; + font-size: 14px; + display: inline-block; + border-radius: 4px; +`; + +export const TaskActions = styled.div` + display: flex; + align-items: center; +`; + +export const TaskAction = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + vertical-align: middle; + line-height: 1; + white-space: nowrap; + cursor: pointer; + user-select: none; + font-size: 14.5px; + color: rgb(66, 82, 110); + font-family: CircularStdBook; + font-weight: normal; + padding: 0px 9px; + border-radius: 3px; + transition: all 0.1s ease 0s; + background: rgb(255, 255, 255); +`; + +export const TaskDetailsWrapper = styled.div` + display: flex; + padding: 0px 30px 60px; +`; + +export const TaskDetailsContent = styled.div` + width: 65%; + padding-right: 50px; +`; + +export const TaskDetailsSidebar = styled.div` + width: 35%; + padding-top: 5px; +`; + +export const TaskDetailsTitleWrapper = styled.div` + height: 44px; + width: 100%; + margin: 18px 0px 0px -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: 7px 7px 8px; + background: rgb(255, 255, 255); + border-width: 1px; + border-style: solid; + border-color: transparent; + border-image: initial; + transition: background 0.1s ease 0s; + overflow-y: hidden; + width: 100%; + color: rgb(23, 43, 77); + &:hover { + background: rgb(235, 236, 240); + } + &:focus { + box-shadow: rgb(76, 154, 255) 0px 0px 0px 1px; + background: rgb(255, 255, 255); + border-width: 1px; + border-style: solid; + border-color: rgb(76, 154, 255); + border-image: initial; + } +`; + +export const TaskDetailsLabel = styled.div` + padding: 20px 0px 12px; + font-size: 15px; + font-weight: 600; +`; + +export const TaskDetailsAddDetailsButton = styled.div` + background-color: rgba(9, 30, 66, 0.04); + box-shadow: none; + border: none; + border-radius: 3px; + display: block; + min-height: 56px; + padding: 8px 12px; + text-decoration: none; + font-size: 14px; + cursor: pointer; + &:hover { + background-color: rgba(9, 30, 66, 0.08); + 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; + background: #fff; + box-shadow: none; + border-color: rgba(9, 30, 66, 0.13); + border-radius: 3px; + line-height: 20px; + padding: 8px 12px; + outline: none; + border: none; + + &:focus { + background: #fff; + border: none; + box-shadow: inset 0 0 0 2px #0079bf; + } +`; + +export const TaskDetailsMarkdown = styled.div` + width: 100%; + cursor: pointer; +`; + +export const TaskDetailsControls = styled.div` + clear: both; + margin-top: 8px; +`; + +export const ConfirmSave = styled.div` + background-color: #5aac44; + box-shadow: none; + border: none; + color: #fff; + float: left; + margin: 0 4px 0 0; + cursor: pointer; + display: inline-block; + font-weight: 400; + line-height: 20px; + padding: 6px 12px; + text-align: center; + border-radius: 3px; + font-size: 14px; +`; + +export const CancelEdit = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + cursor: pointer; +`; diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx new file mode 100644 index 0000000..101c835 --- /dev/null +++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import NormalizeStyles from 'App/NormalizeStyles'; +import BaseStyles from 'App/BaseStyles'; +import styled from 'styled-components'; +import 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 ( + <> + + + { + return ( + setDescription(desc)} + /> + ); + }} + /> + + ); +}; diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx new file mode 100644 index 0000000..0e43368 --- /dev/null +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -0,0 +1,130 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Bin, Cross } from 'shared/icons'; +import useOnOutsideClick from 'shared/hooks/onOutsideClick'; + +import { + TaskActions, + TaskAction, + TaskMeta, + TaskHeader, + TaskDetailsContent, + TaskDetailsWrapper, + TaskDetailsSidebar, + TaskDetailsTitleWrapper, + TaskDetailsTitle, + TaskDetailsLabel, + TaskDetailsAddDetailsButton, + TaskDetailsEditor, + TaskDetailsEditorWrapper, + TaskDetailsMarkdown, + TaskDetailsControls, + ConfirmSave, + CancelEdit, +} from './Styles'; + +type TaskContentProps = { + onEditContent: () => void; + description: string; +}; + +const TaskContent: React.FC = ({ description, onEditContent }) => { + return description === '' ? ( + Add a more detailed description + ) : ( + {description} + ); +}; + +type DetailsEditorProps = { + description: string; + onTaskDescriptionChange: (newDescription: string) => void; + onCancel: () => void; +}; + +const DetailsEditor: React.FC = ({ + description: initialDescription, + onTaskDescriptionChange, + onCancel, +}) => { + const [description, setDescription] = useState(initialDescription); + const $editorWrapperRef = useRef(null); + const $editorRef = useRef(null); + const handleOutsideClick = () => { + onTaskDescriptionChange(description); + }; + useEffect(() => { + if ($editorRef && $editorRef.current) { + $editorRef.current.focus(); + $editorRef.current.select(); + } + }, []); + + useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null); + return ( + + ) => setDescription(e.currentTarget.value)} + /> + + Save + + + + + + ); +}; + +type TaskDetailsProps = { + task: Task; + onTaskDescriptionChange: (task: Task, newDescription: string) => void; +}; + +const TaskDetails: React.FC = ({ task, onTaskDescriptionChange }) => { + const [editorOpen, setEditorOpen] = useState(false); + const handleClick = () => { + setEditorOpen(!editorOpen); + }; + return ( + <> + + + + + + + + + + + + + + + + + Description + {editorOpen ? ( + { + setEditorOpen(false); + onTaskDescriptionChange(task, newDescription); + }} + onCancel={() => { + setEditorOpen(false); + }} + /> + ) : ( + + )} + + + + + ); +}; + +export default TaskDetails; diff --git a/web/src/shared/icons/Bin.tsx b/web/src/shared/icons/Bin.tsx new file mode 100644 index 0000000..31b3637 --- /dev/null +++ b/web/src/shared/icons/Bin.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +type Props = { + size: number | string; + color: string; +}; + +const Bin = ({ size, color }: Props) => { + return ( + + + + + ); +}; + +Bin.defaultProps = { + size: 16, + color: '#000', +}; + +export default Bin; diff --git a/web/src/shared/icons/index.ts b/web/src/shared/icons/index.ts index 66349e4..b0c006c 100644 --- a/web/src/shared/icons/index.ts +++ b/web/src/shared/icons/index.ts @@ -1,5 +1,6 @@ import Cross from './Cross'; import Bell from './Bell'; +import Bin from './Bin'; import Pencil from './Pencil'; import Checkmark from './Checkmark'; import User from './User'; @@ -11,4 +12,4 @@ import Stack from './Stack'; import Question from './Question'; import Exit from './Exit'; -export { Cross, Bell, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock }; +export { Cross, Bell, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };