From f16cceb0e1daddd99abe4e2da906616c27b8cc75 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Wed, 30 Dec 2020 19:12:02 -0600 Subject: [PATCH] feat: add ui skeleton to Task Details while loading --- .../src/Projects/Project/Details/index.tsx | 9 +- .../components/TaskDetails/CommentCreator.tsx | 22 +-- .../shared/components/TaskDetails/Loading.tsx | 162 ++++++++++++++++++ .../shared/components/TaskDetails/Styles.ts | 95 +++++++--- 4 files changed, 251 insertions(+), 37 deletions(-) create mode 100644 frontend/src/shared/components/TaskDetails/Loading.tsx diff --git a/frontend/src/Projects/Project/Details/index.tsx b/frontend/src/Projects/Project/Details/index.tsx index e6441e8..a325c5c 100644 --- a/frontend/src/Projects/Project/Details/index.tsx +++ b/frontend/src/Projects/Project/Details/index.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import Modal from 'shared/components/Modal'; import TaskDetails from 'shared/components/TaskDetails'; +import TaskDetailsLoading from 'shared/components/TaskDetails/Loading'; import { Popup, usePopup } from 'shared/components/PopupMenu'; import MemberManager from 'shared/components/MemberManager'; import { useRouteMatch, useHistory } from 'react-router'; @@ -407,9 +408,7 @@ const Details: React.FC = ({ }); const [updateTaskComment] = useUpdateTaskCommentMutation(); const [editableComment, setEditableComment] = useState(null); - if (!data) { - return null; - } + const isLoading = true; return ( <> = ({ history.push(projectURL); }} renderContent={() => { - return ( + return data ? ( setEditableComment(null)} onUpdateComment={(commentID, message) => { @@ -647,6 +646,8 @@ const Details: React.FC = ({ ); }} /> + ) : ( + ); }} /> diff --git a/frontend/src/shared/components/TaskDetails/CommentCreator.tsx b/frontend/src/shared/components/TaskDetails/CommentCreator.tsx index 05ba82e..f568482 100644 --- a/frontend/src/shared/components/TaskDetails/CommentCreator.tsx +++ b/frontend/src/shared/components/TaskDetails/CommentCreator.tsx @@ -1,4 +1,9 @@ import React, { useRef, useState, useEffect } from 'react'; +import { usePopup } from 'shared/components/PopupMenu'; +import useOnOutsideClick from 'shared/hooks/onOutsideClick'; +import { At, Paperclip, Smile } from 'shared/icons'; +import { Picker, Emoji } from 'emoji-mart'; +import Task from 'shared/icons/Task'; import { CommentTextArea, CommentEditorContainer, @@ -8,11 +13,6 @@ import { CommentProfile, CommentInnerWrapper, } from './Styles'; -import { usePopup } from 'shared/components/PopupMenu'; -import useOnOutsideClick from 'shared/hooks/onOutsideClick'; -import { At, Paperclip, Smile } from 'shared/icons'; -import { Picker, Emoji } from 'emoji-mart'; -import Task from 'shared/icons/Task'; type CommentCreatorProps = { me?: TaskUser; @@ -21,10 +21,12 @@ type CommentCreatorProps = { message?: string | null; onCreateComment: (message: string) => void; onCancelEdit?: () => void; + disabled?: boolean; }; const CommentCreator: React.FC = ({ me, + disabled = false, message, onMemberProfile, onCreateComment, @@ -70,6 +72,7 @@ const CommentCreator: React.FC = ({ showCommentActions={showCommentActions} placeholder="Write a comment..." ref={$comment} + disabled={disabled} value={comment} onChange={e => setComment(e.currentTarget.value)} onFocus={() => { @@ -91,12 +94,11 @@ const CommentCreator: React.FC = ({
{ - console.log(emoji); if ($comment && $comment.current) { - let textToInsert = `${emoji.colons} `; - let cursorPosition = $comment.current.selectionStart; - let textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition); - let textAfterCursorPosition = $comment.current.value.substring( + const textToInsert = `${emoji.colons} `; + const cursorPosition = $comment.current.selectionStart; + const textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition); + const textAfterCursorPosition = $comment.current.value.substring( cursorPosition, $comment.current.value.length, ); diff --git a/frontend/src/shared/components/TaskDetails/Loading.tsx b/frontend/src/shared/components/TaskDetails/Loading.tsx new file mode 100644 index 0000000..419cc0c --- /dev/null +++ b/frontend/src/shared/components/TaskDetails/Loading.tsx @@ -0,0 +1,162 @@ +import React, { useState, useRef } from 'react'; +import { + Plus, + User, + Trash, + Paperclip, + Clone, + Share, + Tags, + Checkmark, + CheckSquareOutline, + At, + Smile, +} from 'shared/icons'; +import { toArray } from 'react-emoji-render'; +import DOMPurify from 'dompurify'; +import TaskAssignee from 'shared/components/TaskAssignee'; +import useOnOutsideClick from 'shared/hooks/onOutsideClick'; +import { usePopup } from 'shared/components/PopupMenu'; +import CommentCreator from 'shared/components/TaskDetails/CommentCreator'; +import { AngleDown } from 'shared/icons/AngleDown'; +import Editor from 'rich-markdown-editor'; +import dark from 'shared/utils/editorTheme'; +import styled from 'styled-components'; +import ReactMarkdown from 'react-markdown'; +import { Picker, Emoji } from 'emoji-mart'; +import 'emoji-mart/css/emoji-mart.css'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import dayjs from 'dayjs'; +import Task from 'shared/icons/Task'; +import { + ActivityItemHeader, + ActivityItemTimestamp, + ActivityItem, + ActivityItemCommentAction, + ActivityItemCommentActions, + TaskDetailLabel, + CommentContainer, + ActivityItemCommentContainer, + MetaDetailContent, + TaskDetailsAddLabelIcon, + ActionButton, + AssignUserIcon, + AssignUserLabel, + AssignUsersButton, + AssignedUsersSection, + ViewRawButton, + DueDateTitle, + Container, + LeftSidebar, + SidebarSkeleton, + ContentContainer, + LeftSidebarContent, + LeftSidebarSection, + SidebarTitle, + SidebarButton, + SidebarButtonText, + MarkCompleteButton, + HeaderContainer, + HeaderLeft, + HeaderInnerContainer, + TaskDetailsTitleWrapper, + TaskDetailsTitle, + ExtraActionsSection, + HeaderRight, + HeaderActionIcon, + EditorContainer, + InnerContentContainer, + DescriptionContainer, + Labels, + ChecklistSection, + MemberList, + TaskMember, + TabBarSection, + TabBarItem, + ActivitySection, + TaskDetailsEditor, + ActivityItemHeaderUser, + ActivityItemHeaderTitle, + ActivityItemHeaderTitleName, + ActivityItemComment, +} from './Styles'; + +type TaskDetailsProps = {}; + +const TaskDetailsLoading: React.FC = () => { + return ( + + + + + TASK GROUP + + + + DUE DATE + + + + + + MEMBERS + + + + + + ACTIONS + }> + Labels + + }> + Checklist + + Cover + + + + + + + + + + Mark complete + + + + + + + + + + + + + + + + + + + + + + + + + Activity + + + + + null} onMemberProfile={() => null} /> + + + + ); +}; + +export default TaskDetailsLoading; diff --git a/frontend/src/shared/components/TaskDetails/Styles.ts b/frontend/src/shared/components/TaskDetails/Styles.ts index 0918e89..903d178 100644 --- a/frontend/src/shared/components/TaskDetails/Styles.ts +++ b/frontend/src/shared/components/TaskDetails/Styles.ts @@ -1,8 +1,9 @@ -import styled, { css } from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; import TextareaAutosize from 'react-autosize-textarea'; import { mixin } from 'shared/utils/styles'; import Button from 'shared/components/Button'; import TaskAssignee from 'shared/components/TaskAssignee'; +import theme from 'App/ThemeStyles'; export const Container = styled.div` display: flex; @@ -16,7 +17,7 @@ export const LeftSidebar = styled.div` background: #222740; `; -export const MarkCompleteButton = styled.button<{ invert: boolean }>` +export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: boolean }>` padding: 4px 8px; position: relative; border: none; @@ -62,6 +63,11 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>` color: ${props.theme.colors.success}; } `} + ${props => + props.invert && + css` + opacity: 0.6; + `} `; export const LeftSidebarContent = styled.div` @@ -89,24 +95,55 @@ export const SidebarTitle = styled.div` text-transform: uppercase; `; -export const SidebarButton = styled.div` +export const defaultBaseColor = theme.colors.bg.primary; + +export const defaultHighlightColor = mixin.lighten(theme.colors.bg.primary, 0.25); + +export const skeletonKeyframes = keyframes` + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } + `; + +export const SidebarButton = styled.div<{ loading?: boolean }>` font-size: 14px; color: ${props => props.theme.colors.text.primary}; min-height: 32px; width: 100%; - - padding: 9px 8px 7px 8px; - border-color: transparent; border-radius: 6px; - border-width: 1px; - border-style: solid; + + ${props => + props.loading + ? css` + background: ${props.theme.colors.bg.primary}; + ` + : css` + padding: 9px 8px 7px 8px; + cursor: pointer; + border-color: transparent; + border-width: 1px; + border-style: solid; + &:hover { + border-color: #414561; + } + `}; display: inline-block; outline: 0; - cursor: pointer; - &:hover { - border-color: #414561; - } +`; + +export const SidebarSkeleton = styled.div` + background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor}); + background-size: 200px 100%; + background-repeat: no-repeat; + border-radius: 6px; + padding: 1px; + animation: ${skeletonKeyframes} 1.2s ease-in-out infinite; + width: 100%; + height: 100%; `; export const SidebarButtonText = styled.span` @@ -141,18 +178,18 @@ export const HeaderLeft = styled.div` justify-content: flex-start; `; -export const TaskDetailsTitleWrapper = styled.div` +export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>` width: 100%; margin: 8px 0 4px 0; - display: inline-block; + display: flex; + border-radius: 6px; + ${props => props.loading && `background: ${props.theme.colors.bg.primary};`} `; -export const TaskDetailsTitle = styled(TextareaAutosize)` +export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>` padding: 9px 8px 7px 8px; border-color: transparent; border-radius: 6px; - border-width: 1px; - border-style: solid; width: 100%; color: #c2c6dc; display: inline-block; @@ -161,13 +198,25 @@ export const TaskDetailsTitle = styled(TextareaAutosize)` font-weight: 700; background: none; - &:hover { - border-color: #414561; - } + ${props => + props.loading + ? css` + background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor}); + background-size: 200px 100%; + background-repeat: no-repeat; + animation: ${skeletonKeyframes} 1.2s ease-in-out infinite; + ` + : css` + &:hover { + border-color: #414561; + border-width: 1px; + border-style: solid; + } - &:focus { - border-color: ${props => props.theme.colors.primary}; - } + &:focus { + border-color: ${props.theme.colors.primary}; + } + `} `; export const DueDateTitle = styled.div`