diff --git a/web/src/App/BaseStyles.ts b/web/src/App/BaseStyles.ts index 1febcbf..e96a068 100644 --- a/web/src/App/BaseStyles.ts +++ b/web/src/App/BaseStyles.ts @@ -70,7 +70,7 @@ export default createGlobalStyle` outline: none; } &:disabled { - opacity: 1; + opacity: 0.5; } } [role="button"], button, input, textarea { diff --git a/web/src/App/ThemeStyles.ts b/web/src/App/ThemeStyles.ts new file mode 100644 index 0000000..1d46126 --- /dev/null +++ b/web/src/App/ThemeStyles.ts @@ -0,0 +1,47 @@ +import { createGlobalStyle, DefaultTheme } from 'styled-components'; + +const theme: DefaultTheme = { + borderRadius: { + primary: '3px', + alternate: '6px', + }, + colors: { + primary: '115, 103, 240', + secondary: '216, 93, 216', + alternate: '65, 69, 97', + success: '40, 199, 111', + danger: '234, 84, 85', + warning: '255, 159, 67', + dark: '30, 30, 30', + text: { + primary: '194, 198, 220', + secondary: '255, 255, 255', + }, + bg: { + primary: '16, 22, 58', + secondary: '38, 44, 73', + }, + }, +}; + +export { theme }; + +export default createGlobalStyle` + :root { + --color-text: #c2c6dc; + --color-text-hover: #fff; + --color-primary: rgba(115, 103, 240); + --color-button-text: #c2c6dc; + --color-button-text-hover: #fff; + --color-button-background: rgba(115, 103, 240); + + --color-background: #262c49; + --color-background-dark: #10163a; + + --color-input-text: #c2c6dc; + --color-input-text-focus: #fff; + + --color-icon: #c2c6dc; + --color-active-icon: rgba(115, 103, 240); + } +`; diff --git a/web/src/App/index.tsx b/web/src/App/index.tsx index 557faf8..e34af90 100644 --- a/web/src/App/index.tsx +++ b/web/src/App/index.tsx @@ -2,9 +2,10 @@ import React, { useState, useEffect } from 'react'; import jwtDecode from 'jwt-decode'; import { createBrowserHistory } from 'history'; import { setAccessToken } from 'shared/utils/accessToken'; -import styled from 'styled-components'; +import styled, { ThemeProvider } from 'styled-components'; import NormalizeStyles from './NormalizeStyles'; import BaseStyles from './BaseStyles'; +import ThemeStyles, { theme } from './ThemeStyles'; import Routes from './Routes'; import { UserIDContext } from './context'; import Navbar from './Navbar'; @@ -39,20 +40,23 @@ const App = () => { return ( <> - - - - - {loading ? ( -
loading
- ) : ( - <> - - - - )} -
-
+ + + + + + + {loading ? ( +
loading
+ ) : ( + <> + + + + )} +
+
+
); diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index 368311a..4996054 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -229,14 +229,14 @@ const ProjectAction = styled.div` display: flex; align-items: center; font-size: 15px; - color: #c2c6dc; + color: var(--color-text); &:not(:last-child) { margin-right: 16px; } &:hover { - color: ${mixin.lighten('#c2c6dc', 0.25)}; + color: var(--color-text-hover); } `; @@ -490,15 +490,15 @@ const Project = () => { ); }} > - + Labels - + Fields - + Rules @@ -588,8 +588,8 @@ const Project = () => { setQuickCardEditor(initialQuickCardEditorState)} - onEditCard={(_listId: string, cardId: string, cardName: string) => { - updateTaskName({ variables: { taskID: cardId, name: cardName } }); + onEditCard={(_taskGroupID: string, taskID: string, cardName: string) => { + updateTaskName({ variables: { taskID, name: cardName } }); }} onOpenMembersPopup={($targetRef, task) => { showPopup( diff --git a/web/src/index.tsx b/web/src/index.tsx index f72da2f..b05a5fb 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,20 +1,22 @@ import React from 'react'; - import ReactDOM from 'react-dom'; +import axios from 'axios'; +import createAuthRefreshInterceptor from 'axios-auth-refresh'; import { ApolloProvider } from '@apollo/react-hooks'; import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { HttpLink } from 'apollo-link-http'; import { onError } from 'apollo-link-error'; import { ApolloLink, Observable, fromPromise } from 'apollo-link'; - import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken'; -import axios from 'axios'; -import createAuthRefreshInterceptor from 'axios-auth-refresh'; - import App from './App'; -// Function that will be called to refresh authorization +// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8 + +let forward$; +let isRefreshing = false; +let pendingRequests: any = []; + const refreshAuthLogic = (failedRequest: any) => axios.post('http://localhost:3333/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => { setAccessToken(tokenRefreshResponse.data.accessToken); @@ -24,12 +26,6 @@ const refreshAuthLogic = (failedRequest: any) => createAuthRefreshInterceptor(axios, refreshAuthLogic); -// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8 - -let forward$; -let isRefreshing = false; -let pendingRequests: any = []; - const resolvePendingRequests = () => { pendingRequests.map((callback: any) => callback()); pendingRequests = []; diff --git a/web/src/shared/components/AddList/Styles.ts b/web/src/shared/components/AddList/Styles.ts index 563d258..09b50fc 100644 --- a/web/src/shared/components/AddList/Styles.ts +++ b/web/src/shared/components/AddList/Styles.ts @@ -1,6 +1,7 @@ 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``; @@ -90,22 +91,10 @@ export const ListAddControls = styled.div` margin: 4px 0 0; `; -export const AddListButton = styled.button` - box-shadow: none; - border: none; - color: #c2c6dc; +export const AddListButton = styled(Button)` 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; - - background: rgb(115, 103, 240); `; export const CancelAdd = styled.div` diff --git a/web/src/shared/components/AddList/index.tsx b/web/src/shared/components/AddList/index.tsx index c597941..e9d9272 100644 --- a/web/src/shared/components/AddList/index.tsx +++ b/web/src/shared/components/AddList/index.tsx @@ -50,6 +50,7 @@ const NameEditor: React.FC = ({ onSave, onCancel }) => { { onSave(listName); setListName(''); diff --git a/web/src/shared/components/Button/Button.stories.tsx b/web/src/shared/components/Button/Button.stories.tsx new file mode 100644 index 0000000..d7edc81 --- /dev/null +++ b/web/src/shared/components/Button/Button.stories.tsx @@ -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 ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/web/src/shared/components/Button/index.tsx b/web/src/shared/components/Button/index.tsx new file mode 100644 index 0000000..5e47954 --- /dev/null +++ b/web/src/shared/components/Button/index.tsx @@ -0,0 +1,171 @@ +import React from 'react'; +import styled, { css } from 'styled-components/macro'; + +const Text = styled.span<{ fontSize: string }>` + position: relative; + display: inline-block; + 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; + className?: string; + onClick?: () => void; +}; + +const Button: React.FC = ({ + disabled = false, + fontSize = '14px', + color = 'primary', + variant = 'filled', + onClick, + className, + children, +}) => { + const handleClick = () => { + if (onClick) { + onClick(); + } + }; + switch (variant) { + case 'filled': + return ( + + {children} + + ); + case 'outline': + return ( + + {children} + + ); + case 'flat': + return ( + + {children} + + ); + case 'lineDown': + return ( + + {children} + + + ); + case 'gradient': + return ( + + {children} + + ); + case 'relief': + return ( + + {children} + + ); + default: + throw new Error('not a valid variant'); + } +}; + +export default Button; diff --git a/web/src/shared/components/Card/Card.stories.tsx b/web/src/shared/components/Card/Card.stories.tsx index d499ec0..9bebbc0 100644 --- a/web/src/shared/components/Card/Card.stories.tsx +++ b/web/src/shared/components/Card/Card.stories.tsx @@ -107,9 +107,68 @@ export const Everything = () => { 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(null); + return ( + + ); +}; + +export const Editable = () => { + const $ref = useRef(null); + return ( + + ); +}; diff --git a/web/src/shared/components/Card/Styles.ts b/web/src/shared/components/Card/Styles.ts index 14f4cd7..5ca43fc 100644 --- a/web/src/shared/components/Card/Styles.ts +++ b/web/src/shared/components/Card/Styles.ts @@ -1,10 +1,34 @@ 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 { 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: 16px; + line-height: 20px; + color: var(--color-input-text-focus); + &:focus { + border: none; + outline: none; + } +`; + export const ListCardBadges = styled.div` float: left; display: flex; @@ -17,6 +41,7 @@ export const ListCardBadge = styled.div` display: flex; align-items: center; margin: 0 6px 4px 0; + font-size: 12px; max-width: 100%; min-height: 20px; overflow: hidden; @@ -32,6 +57,7 @@ export const DescriptionBadge = styled(ListCardBadge)` `; export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>` + font-size: 12px; ${props => props.isPastDue && css` @@ -49,16 +75,16 @@ export const ListCardBadgeText = styled.span` white-space: nowrap; `; -export const ListCardContainer = styled.div<{ isActive: boolean }>` +export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>` max-width: 256px; margin-bottom: 8px; - background-color: #fff; border-radius: 3px; ${mixin.boxShadowCard} cursor: pointer !important; position: relative; - background-color: ${props => (props.isActive ? mixin.darken('#262c49', 0.1) : mixin.lighten('#262c49', 0.05))}; + background-color: ${props => + props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : 'var(--color-background)'}; `; export const ListCardInnerContainer = styled.div` @@ -113,18 +139,16 @@ export const ListCardOperation = styled.span` `; export const CardTitle = styled.span` - font-family: 'Droid Sans'; clear: both; display: block; margin: 0 0 4px; overflow: hidden; text-decoration: none; word-wrap: break-word; - color: #c2c6dc; + color: var(--color-text); `; export const CardMembers = styled.div` float: right; - margin: 0 -2px 0 0; + margin: 0 -2px 4px 0; `; - diff --git a/web/src/shared/components/Card/index.tsx b/web/src/shared/components/Card/index.tsx index d8c6c22..88a3ddc 100644 --- a/web/src/shared/components/Card/index.tsx +++ b/web/src/shared/components/Card/index.tsx @@ -1,11 +1,10 @@ -import React, { useState, useRef } from 'react'; -import { DraggableProvidedDraggableProps } from 'react-beautiful-dnd'; -import PropTypes from 'prop-types'; +import React, { useState, useRef, useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Member from 'shared/components/Member'; +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, DescriptionBadge, DueDateCardBadge, ListCardBadges, @@ -21,7 +20,6 @@ import { CardTitle, CardMembers, } from './Styles'; -import TaskAssignee from 'shared/components/TaskAssignee'; type DueDate = { isPastDue: boolean; @@ -35,11 +33,11 @@ type Checklist = { type Props = { title: string; - description: string; taskID: string; taskGroupID: string; - onContextMenu: (e: ContextMenuEvent) => void; - onClick: (e: React.MouseEvent) => void; + onContextMenu?: (e: ContextMenuEvent) => void; + onClick?: (e: React.MouseEvent) => void; + description?: null | string; dueDate?: DueDate; checklists?: Checklist; labels?: Array; @@ -47,6 +45,9 @@ type Props = { wrapperProps?: any; members?: Array | null; onCardMemberClick?: OnCardMemberClick; + editable?: boolean; + onEditCard?: (taskGroupID: string, taskID: string, cardName: string) => void; + onCardTitleChange?: (name: string) => void; }; const Card = React.forwardRef( @@ -65,20 +66,47 @@ const Card = React.forwardRef( 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 (typeof $innerCardRef.current !== 'undefined') { const pos = $innerCardRef.current.getBoundingClientRect(); - onContextMenu({ - top: pos.top, - left: pos.left, - taskGroupID, - taskID, - }); + if (onContextMenu) { + onContextMenu({ + top: pos.top, + left: pos.left, + taskGroupID, + taskID, + }); + } } }; const onTaskContext = (e: React.MouseEvent) => { @@ -96,9 +124,14 @@ const Card = React.forwardRef( onMouseEnter={() => setActive(true)} onMouseLeave={() => setActive(false)} ref={$cardRef} - onClick={onClick} + onClick={e => { + if (onClick) { + onClick(e); + } + }} onContextMenu={onTaskContext} isActive={isActive} + editable={editable} {...wrapperProps} > @@ -116,7 +149,24 @@ const Card = React.forwardRef( ))} - {title} + {editable ? ( + { + setCardTitle(e.currentTarget.value); + if (onCardTitleChange) { + onCardTitleChange(e.currentTarget.value); + } + }} + onClick={e => { + e.stopPropagation(); + }} + onKeyDown={handleKeyDown} + value={currentCardTitle} + ref={$editorRef} + /> + ) : ( + {title} + )} {watched && ( diff --git a/web/src/shared/components/CardComposer/Styles.ts b/web/src/shared/components/CardComposer/Styles.ts index 56745b8..4ffd6da 100644 --- a/web/src/shared/components/CardComposer/Styles.ts +++ b/web/src/shared/components/CardComposer/Styles.ts @@ -1,4 +1,5 @@ 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'; @@ -15,53 +16,6 @@ export const CardComposerWrapper = styled.div<{ isOpen: boolean }>` flex-direction: column; `; -export const ListCard = styled.div` - background-color: ${props => mixin.lighten('#262c49', 0.05)}; - border-radius: 3px; - ${mixin.boxShadowCard} - cursor: pointer; - display: block; - margin-bottom: 8px; - max-width: 300px; - min-height: 20px; - position: relative; - text-decoration: none; - z-index: 0; -`; - -export const ListCardDetails = styled.div` - overflow: hidden; - padding: 6px 8px 2px; - position: relative; - z-index: 10; -`; - -export const ListCardLabels = styled.div``; - -export const ListCardEditor = styled(TextareaAutosize)` - font-family: 'Droid Sans'; - overflow: hidden; - overflow-wrap: break-word; - resize: none; - height: 54px; - width: 100%; - - background: none; - border: none; - box-shadow: none; - margin-bottom: 4px; - max-height: 162px; - min-height: 54px; - padding: 0; - font-size: 14px; - line-height: 20px; - - color: #c2c6dc; - l &:focus { - background-color: ${props => mixin.lighten('#262c49', 0.05)}; - } -`; - export const ComposerControls = styled.div``; export const ComposerControlsSaveSection = styled.div` @@ -73,18 +27,9 @@ export const ComposerControlsSaveSection = styled.div` export const ComposerControlsActionsSection = styled.div` float: right; `; - -export const AddCardButton = styled.button` - background: rgb(115, 103, 240); - box-shadow: none; - border: none; - color: #c2c6dc; - cursor: pointer; - display: inline-block; - font-weight: 400; - line-height: 20px; +export const AddCardButton = styled(Button)` margin-right: 4px; padding: 6px 12px; - text-align: center; border-radius: 3px; `; + diff --git a/web/src/shared/components/CardComposer/index.tsx b/web/src/shared/components/CardComposer/index.tsx index faffeef..5303e2f 100644 --- a/web/src/shared/components/CardComposer/index.tsx +++ b/web/src/shared/components/CardComposer/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +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'; @@ -8,13 +8,11 @@ import { CardComposerWrapper, CancelIcon, AddCardButton, - ListCard, - ListCardDetails, - ListCardEditor, ComposerControls, ComposerControlsSaveSection, ComposerControlsActionsSection, } from './Styles'; +import Card from '../Card'; type Props = { isOpen: boolean; @@ -24,43 +22,36 @@ type Props = { const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => { const [cardName, setCardName] = useState(''); - const $cardEditor: any = useRef(null); - const handleCreateCard = () => { - onCreateCard(cardName); - setCardName(''); - if ($cardEditor && $cardEditor.current) { - $cardEditor.current.focus(); - } - }; - const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleCreateCard(); - } - }; + const $cardRef = useRef(null); + useOnOutsideClick($cardRef, true, onClose, null); useOnEscapeKeyDown(isOpen, onClose); - useOnOutsideClick($cardEditor, true, () => onClose(), null); - useEffect(() => { - $cardEditor.current.focus(); - }, []); return ( - - - { - setCardName(e.currentTarget.value); - }} - value={cardName} - placeholder="Enter a title for this card..." - /> - - + { + onCreateCard(name); + setCardName(''); + }} + onCardTitleChange={name => { + setCardName(name); + }} + /> - Add Card + { + onCreateCard(cardName); + setCardName(''); + }} + > + Add Card + diff --git a/web/src/shared/components/Input/Input.stories.tsx b/web/src/shared/components/Input/Input.stories.tsx new file mode 100644 index 0000000..32cd832 --- /dev/null +++ b/web/src/shared/components/Input/Input.stories.tsx @@ -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 ( + <> + + + + + + + } width="100%" placeholder="Placeholder" /> + + + + ); +}; diff --git a/web/src/shared/components/Input/index.tsx b/web/src/shared/components/Input/index.tsx new file mode 100644 index 0000000..d596752 --- /dev/null +++ b/web/src/shared/components/Input/index.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import styled 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: 17px; +`; + +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<{ 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%); + } +`; + +const Icon = styled.div` + display: flex; + left: 16px; + position: absolute; +`; + +type InputProps = { + variant?: 'normal' | 'alternate'; + label?: string; + width?: string; + placeholder?: string; + icon?: JSX.Element; +}; + +const Input: React.FC = ({ width = 'auto', variant = 'normal', label, placeholder, icon }) => { + const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; + const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; + return ( + + + {label && {label}} + {icon && icon} + + ); +}; + +export default Input; diff --git a/web/src/shared/components/Lists/Lists.stories.tsx b/web/src/shared/components/Lists/Lists.stories.tsx index 7878d21..bdcdf47 100644 --- a/web/src/shared/components/Lists/Lists.stories.tsx +++ b/web/src/shared/components/Lists/Lists.stories.tsx @@ -65,7 +65,6 @@ const initialListsData = { export const Default = () => { const [listsData, setListsData] = useState(initialListsData); const onCardDrop = (droppedTask: Task) => { - console.log(droppedTask); const newState = { ...listsData, tasks: { @@ -85,84 +84,6 @@ export const Default = () => { }; setListsData(newState); }; - return ; -}; - -const createColumn = (id: any, name: any, position: any) => { - return { - taskGroupID: id, - name, - position, - tasks: [], - }; -}; - -const initialListsDataLarge = { - columns: { - 'column-1': createColumn('column-1', 'General', 1), - 'column-2': createColumn('column-2', 'General', 2), - 'column-3': createColumn('column-3', 'General', 3), - 'column-4': createColumn('column-4', 'General', 4), - 'column-5': createColumn('column-5', 'General', 5), - 'column-6': createColumn('column-6', 'General', 6), - 'column-7': createColumn('column-7', 'General', 7), - 'column-8': createColumn('column-8', 'General', 8), - 'column-9': createColumn('column-9', 'General', 9), - }, - tasks: { - 'task-1': { - taskID: 'task-1', - 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 ListsWithManyList = () => { - const [listsData, setListsData] = useState(initialListsDataLarge); - const onCardDrop = (droppedTask: any) => { - const newState = { - ...listsData, - tasks: { - ...listsData.tasks, - [droppedTask.id]: droppedTask, - }, - }; - setListsData(newState); - }; - const onListDrop = (droppedColumn: any) => { - const newState = { - ...listsData, - columns: { - ...listsData.columns, - [droppedColumn.id]: droppedColumn, - }, - }; - setListsData(newState); - }; return ( = [ 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 [top, setTop] = useState(0); const [left, setLeft] = useState(0); @@ -44,15 +53,7 @@ export const Default = () => { <> {isEditorOpen && ( setEditorOpen(false)} onEditCard={action('edit card')} onOpenLabelsPopup={action('open popup')} @@ -76,7 +77,7 @@ export const Default = () => { taskGroupID="1" description="hello!" ref={$cardRef} - title="Hello, world" + title={task.name} onClick={action('on click')} onContextMenu={e => { setTop(e.top); diff --git a/web/src/shared/components/QuickCardEditor/Styles.ts b/web/src/shared/components/QuickCardEditor/Styles.ts index d0422df..22a7c62 100644 --- a/web/src/shared/components/QuickCardEditor/Styles.ts +++ b/web/src/shared/components/QuickCardEditor/Styles.ts @@ -21,53 +21,6 @@ export const Container = styled.div<{ top: number; left: number }>` left: ${props => props.left}px; `; -export const Editor = styled.div` - background-color: ${props => mixin.lighten('#262c49', 0.05)}; - border-radius: 3px; - box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25); - color: #c2c6dc; - padding: 6px 8px 2px; - cursor: default; - display: block; - margin-bottom: 8px; - max-width: 300px; - min-height: 20px; - position: relative; - text-decoration: none; - z-index: 1; -`; - -export const EditorDetails = styled.div` - overflow: hidden; - padding: 0; - position: relative; - z-index: 10; -`; - -export const EditorTextarea = styled(TextareaAutosize)` - font-family: 'Droid Sans'; - overflow: hidden; - overflow-wrap: break-word; - resize: none; - height: 54px; - width: 100%; - - background: none; - border: none; - box-shadow: none; - margin-bottom: 4px; - max-height: 162px; - min-height: 54px; - padding: 0; - font-size: 16px; - line-height: 20px; - color: #fff; - &:focus { - border: none; - outline: none; - } -`; - export const SaveButton = styled.button` cursor: pointer; background: rgb(115, 103, 240); @@ -125,25 +78,3 @@ export const CloseButton = styled.div` z-index: 40; cursor: pointer; `; - -export const ListCardLabels = styled.div` - overflow: auto; - position: relative; -`; - -export const ListCardLabel = styled.span` - height: 16px; - line-height: 16px; - padding: 0 8px; - max-width: 198px; - float: left; - font-size: 12px; - font-weight: 700; - margin: 0 4px 4px 0; - width: auto; - border-radius: 4px; - color: #fff; - display: block; - position: relative; - background-color: ${props => props.color}; -`; diff --git a/web/src/shared/components/QuickCardEditor/index.tsx b/web/src/shared/components/QuickCardEditor/index.tsx index f1a2e17..529e021 100644 --- a/web/src/shared/components/QuickCardEditor/index.tsx +++ b/web/src/shared/components/QuickCardEditor/index.tsx @@ -1,20 +1,8 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState } from 'react'; import Cross from 'shared/icons/Cross'; import styled from 'styled-components'; -import Member from 'shared/components/Member'; -import { - Wrapper, - Container, - Editor, - EditorDetails, - EditorTextarea, - EditorButtons, - SaveButton, - EditorButton, - CloseButton, - ListCardLabels, - ListCardLabel, -} from './Styles'; +import { Wrapper, Container, EditorButtons, SaveButton, EditorButton, CloseButton } from './Styles'; +import Card from '../Card'; export const CardMembers = styled.div` position: absolute; @@ -46,61 +34,34 @@ const QuickCardEditor = ({ left, }: Props) => { const [currentCardTitle, setCardTitle] = useState(task.name); - const $editorRef: any = useRef(); const $labelsRef: any = useRef(); const $membersRef: any = useRef(); - useEffect(() => { - $editorRef.current.focus(); - $editorRef.current.select(); - }, []); const handleCloseEditor = (e: any) => { e.stopPropagation(); onCloseEditor(); }; - const handleKeyDown = (e: any) => { - if (e.key === 'Enter') { - e.preventDefault(); - onEditCard(task.taskGroup.id, task.id, currentCardTitle); - onCloseEditor(); - } - }; - return ( - - - {task.labels && - task.labels.map(label => ( - - {label.projectLabel.name} - - ))} - - - setCardTitle(e.currentTarget.value)} - onClick={e => { - e.stopPropagation(); - }} - onKeyDown={handleKeyDown} - value={currentCardTitle} - ref={$editorRef} - /> - - {task.assigned && - task.assigned.map(member => ( - - ))} - - - - onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save + { + onEditCard(taskGroupID, taskID, name); + onCloseEditor(); + }} + members={task.assigned} + taskID={task.id} + taskGroupID={task.taskGroup.id} + labels={task.labels.map(l => l.projectLabel)} + /> + onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save = ({ label }) => { - return ( - - - {label} - - ); -}; +import Input from 'shared/components/Input'; +import Button from 'shared/components/Button'; const ProfileContainer = styled.div` display: flex; @@ -103,30 +44,13 @@ const ActionButtons = styled.div` flex-wrap: wrap; align-items: center; `; -const UploadButton = styled.div` +const UploadButton = styled(Button)` margin-right: 1rem; - padding: 0.75rem 2rem; - border: 0; - border-radius: 6px; - cursor: pointer; - position: relative; - overflow: hidden; - color: #fff; display: inline-block; - background: rgba(115, 103, 240); `; -const RemoveButton = styled.button` +const RemoveButton = styled(Button)` display: inline-block; - border: 1px solid rgba(234, 84, 85, 1); - background: transparent; - color: rgba(234, 84, 85, 1); - padding: 0.75rem 2rem; - - border-radius: 6px; - cursor: pointer; - position: relative; - overflow: hidden; `; const ImgLabel = styled.p` @@ -164,7 +88,9 @@ const AvatarSettings: React.FC = ({ profile, onProfileAvata onProfileAvatarChange()}>Upload photo - onProfileAvatarRemove()}>Remove + onProfileAvatarRemove()}> + Remove + Allowed JPG, GIF or PNG. Max size of 800kB @@ -241,6 +167,7 @@ const TabNavLine = styled.span<{ top: number }>` `; const TabContentWrapper = styled.div` + margin-left: 1rem; position: relative; display: block; overflow: hidden; @@ -254,7 +181,6 @@ const TabContent = styled.div` padding: 0; padding: 1.5rem; background-color: #10163a; - margin-left: 1rem !important; border-radius: 0.5rem; `; @@ -294,17 +220,9 @@ const SettingActions = styled.div` justify-content: flex-end; `; -const SaveButton = styled.div` +const SaveButton = styled(Button)` margin-right: 1rem; - padding: 0.75rem 2rem; - border: 0; - border-radius: 6px; - cursor: pointer; - position: relative; - overflow: hidden; - color: #fff; display: inline-block; - background: rgba(115, 103, 240); `; type SettingsProps = { @@ -345,11 +263,11 @@ const Settings: React.FC = ({ onProfileAvatarRemove, onProfileAva onProfileAvatarChange={onProfileAvatarChange} profile={profile} /> - - - - - + + + + + Save Change diff --git a/web/src/shared/components/TaskAssignee/index.tsx b/web/src/shared/components/TaskAssignee/index.tsx index 7a37329..2c82d77 100644 --- a/web/src/shared/components/TaskAssignee/index.tsx +++ b/web/src/shared/components/TaskAssignee/index.tsx @@ -16,10 +16,11 @@ export const Wrapper = styled.div<{ size: number | string; bgColor: string | nul 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; + font-size: 14px; + font-weight: 400; `; type TaskAssigneeProps = { diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts index 75ca0f3..bb3ffe5 100644 --- a/web/src/shared/components/TaskDetails/Styles.ts +++ b/web/src/shared/components/TaskDetails/Styles.ts @@ -1,6 +1,7 @@ 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; @@ -159,23 +160,13 @@ export const TaskDetailsMarkdown = styled.div` export const TaskDetailsControls = styled.div` clear: both; margin-top: 8px; + display: flex; `; -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; +export const ConfirmSave = styled(Button)` padding: 6px 12px; - text-align: center; border-radius: 3px; - font-size: 14px; + margin-right: 6px; `; export const CancelEdit = styled.div` diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 41d7dfa..250d752 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -88,7 +88,9 @@ const DetailsEditor: React.FC = ({ onChange={(e: React.ChangeEvent) => setDescription(e.currentTarget.value)} /> - Save + + Save + diff --git a/web/src/shared/components/Textarea/index.tsx b/web/src/shared/components/Textarea/index.tsx new file mode 100644 index 0000000..eb5f7ce --- /dev/null +++ b/web/src/shared/components/Textarea/index.tsx @@ -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; diff --git a/web/src/shared/components/TopNavbar/Styles.ts b/web/src/shared/components/TopNavbar/Styles.ts index bb9af72..8f4a999 100644 --- a/web/src/shared/components/TopNavbar/Styles.ts +++ b/web/src/shared/components/TopNavbar/Styles.ts @@ -1,6 +1,7 @@ import styled, { css } from 'styled-components'; import TextareaAutosize from 'react-autosize-textarea'; import { mixin } from 'shared/utils/styles'; +import Button from 'shared/components/Button'; export const NavbarWrapper = styled.div` width: 100%; @@ -103,7 +104,7 @@ export const ProjectTabs = styled.div` export const ProjectTab = styled.span<{ active?: boolean }>` font-size: 80%; - color: #c2c6dc; + color: rgba(${props => props.theme.colors.text.primary}); font-size: 15px; cursor: pointer; display: flex; @@ -122,27 +123,25 @@ export const ProjectTab = styled.span<{ active?: boolean }>` ${props => props.active ? css` - box-shadow: inset 0 -2px #d85dd8; - color: #d85dd8; + box-shadow: inset 0 -2px rgba(${props.theme.colors.secondary}); + color: rgba(${props.theme.colors.secondary}); ` : css` &:hover { - box-shadow: inset 0 -2px #cbd4db; - color: ${mixin.lighten('#c2c6dc', 0.25)}; + box-shadow: inset 0 -2px rgba(${props.theme.colors.text.secondary}); + color: rgba(${props.theme.colors.text.secondary}); } `} `; export const ProjectName = styled.h1` - color: #c2c6dc; + color: rgba(${props => props.theme.colors.text.primary}); font-weight: 600; font-size: 20px; padding: 3px 10px 3px 8px; - font-family: 'Droid Sans'; margin: -4px 0; `; export const ProjectNameTextarea = styled(TextareaAutosize)` - font-family: 'Droid Sans'; border: none; resize: none; overflow: hidden; @@ -211,28 +210,7 @@ export const ProjectSettingsButton = styled.button` } `; -export const InviteButton = 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; - +export const InviteButton = styled(Button)` margin: 0 0 0 8px; - - border-radius: 3px; - border-width: 1px; - border-style: solid; - border-color: transparent; - border-image: initial; - border-color: #414561; - - &:hover { - background: rgb(115, 103, 240); - } + padding: 6px 12px; `; diff --git a/web/src/shared/components/TopNavbar/index.tsx b/web/src/shared/components/TopNavbar/index.tsx index bdc0531..7390be3 100644 --- a/web/src/shared/components/TopNavbar/index.tsx +++ b/web/src/shared/components/TopNavbar/index.tsx @@ -176,7 +176,7 @@ const NavBar: React.FC = ({ {projectMembers.map(member => ( ))} - Invite + Invite )} diff --git a/web/src/styled.d.ts b/web/src/styled.d.ts new file mode 100644 index 0000000..6012716 --- /dev/null +++ b/web/src/styled.d.ts @@ -0,0 +1,30 @@ +// import original module declarations +import 'styled-components'; + +// and extend them! +declare module 'styled-components' { + export interface DefaultTheme { + borderRadius: { + primary: string; + alternate: string; + }; + colors: { + [key: string]: any; + primary: string; + secondary: string; + success: string; + danger: string; + warning: string; + dark: string; + alternate: string; + text: { + primary: string; + secondary: string; + }; + bg: { + primary: string; + secondary: string; + }; + }; + } +}