chore: add theme colors, merge Card components, create single Button component

This commit is contained in:
Jordan Knott 2020-06-14 19:50:35 -05:00
parent 6267a37b6e
commit a12e9c1e50
30 changed files with 840 additions and 525 deletions

View File

@ -70,7 +70,7 @@ export default createGlobalStyle`
outline: none;
}
&:disabled {
opacity: 1;
opacity: 0.5;
}
}
[role="button"], button, input, textarea {

View File

@ -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);
}
`;

View File

@ -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,9 +40,11 @@ const App = () => {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<ThemeProvider theme={theme}>
<PopupProvider>
<NormalizeStyles />
<BaseStyles />
<ThemeStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
@ -53,6 +56,7 @@ const App = () => {
)}
</Router>
</PopupProvider>
</ThemeProvider>
</UserIDContext.Provider>
</>
);

View File

@ -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 = () => {
);
}}
>
<Tags size={13} color="#c2c6dc" />
<Tags size={13} color="var(--color-icon)" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ToggleOn size={13} color="var(--color-icon)" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<Bolt size={13} color="var(--color-icon)" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
@ -588,8 +588,8 @@ const Project = () => {
<QuickCardEditor
task={currentQuickTask}
onCloseEditor={() => 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(

View File

@ -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 = [];

View File

@ -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`

View File

@ -50,6 +50,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
</ListNameEditorWrapper>
<ListAddControls>
<AddListButton
variant="relief"
onClick={() => {
onSave(listName);
setListName('');

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,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<ButtonProps> = ({
disabled = false,
fontSize = '14px',
color = 'primary',
variant = 'filled',
onClick,
className,
children,
}) => {
const handleClick = () => {
if (onClick) {
onClick();
}
};
switch (variant) {
case 'filled':
return (
<Filled onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Filled>
);
case 'outline':
return (
<Outline onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Outline>
);
case 'flat':
return (
<Flat onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Flat>
);
case 'lineDown':
return (
<LineDown onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
<LineX color={color} />
</LineDown>
);
case 'gradient':
return (
<Gradient onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Gradient>
);
case 'relief':
return (
<Relief 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

@ -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<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

@ -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;
`;

View File

@ -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<HTMLDivElement>) => void;
onContextMenu?: (e: ContextMenuEvent) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
description?: null | string;
dueDate?: DueDate;
checklists?: Checklist;
labels?: Array<ProjectLabel>;
@ -47,6 +45,9 @@ type Props = {
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(
@ -65,14 +66,40 @@ 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();
if (onContextMenu) {
onContextMenu({
top: pos.top,
left: pos.left,
@ -80,6 +107,7 @@ const Card = React.forwardRef(
taskID,
});
}
}
};
const onTaskContext = (e: React.MouseEvent) => {
e.preventDefault();
@ -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}
>
<ListCardInnerContainer ref={$innerCardRef}>
@ -116,7 +149,24 @@ const Card = React.forwardRef(
</ListCardLabel>
))}
</ListCardLabels>
{editable ? (
<EditorTextarea
onChange={e => {
setCardTitle(e.currentTarget.value);
if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value);
}
}}
onClick={e => {
e.stopPropagation();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
/>
) : (
<CardTitle>{title}</CardTitle>
)}
<ListCardBadges>
{watched && (
<ListCardBadge>

View File

@ -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;
`;

View File

@ -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<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCreateCard();
}
};
const $cardRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($cardRef, true, onClose, null);
useOnEscapeKeyDown(isOpen, onClose);
useOnOutsideClick($cardEditor, true, () => onClose(), null);
useEffect(() => {
$cardEditor.current.focus();
}, []);
return (
<CardComposerWrapper isOpen={isOpen}>
<ListCard>
<ListCardDetails>
<ListCardEditor
onKeyDown={onKeyDown}
ref={$cardEditor}
onChange={e => {
setCardName(e.currentTarget.value);
<Card
title={cardName}
ref={$cardRef}
taskID=""
taskGroupID=""
editable
onEditCard={(_taskGroupID, _taskID, name) => {
onCreateCard(name);
setCardName('');
}}
onCardTitleChange={name => {
setCardName(name);
}}
value={cardName}
placeholder="Enter a title for this card..."
/>
</ListCardDetails>
</ListCard>
<ComposerControls>
<ComposerControlsSaveSection>
<AddCardButton onClick={handleCreateCard}>Add Card</AddCardButton>
<AddCardButton
variant="relief"
onClick={() => {
onCreateCard(cardName);
setCardName('');
}}
>
Add Card
</AddCardButton>
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
</ComposerControlsSaveSection>
<ComposerControlsActionsSection />

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,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<InputProps> = ({ 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 (
<InputWrapper width={width}>
<InputInput
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

@ -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 <span />;
};
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 (
<Lists
taskGroups={[]}

View File

@ -74,11 +74,11 @@ export const LoginButton = styled.input`
padding: 0.75rem 2rem;
font-size: 1rem;
border-radius: 6px;
background: rgb(115, 103, 240);
background: var(--color-button-background);
outline: none;
border: none;
cursor: pointer;
color: #fff;
color: var(--color-button-text-hover);
&:disabled {
opacity: 0.5;
cursor: default;
@ -98,7 +98,7 @@ export const RegisterButton = styled.button`
border: 1px solid rgb(115, 103, 240);
background: transparent;
font-size: 1rem;
color: rgba(115, 103, 240);
color: var(--color-primary);
cursor: pointer;
`;

View File

@ -25,6 +25,7 @@ const CardMemberInitials = styled.div`
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 400;
`;
type MemberProps = {

View File

@ -37,6 +37,15 @@ const labelData: Array<TaskLabel> = [
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 && (
<QuickCardEditor
task={{
id: 'task',
name: 'General',
taskGroup: {
id: '1',
},
position: 1,
labels: labelData,
}}
task={task}
onCloseEditor={() => 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);

View File

@ -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};
`;

View File

@ -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 (
<Wrapper onClick={handleCloseEditor} open>
<CloseButton onClick={handleCloseEditor}>
<Cross size={16} color="#000" />
</CloseButton>
<Container left={left} top={top}>
<Editor>
<ListCardLabels>
{task.labels &&
task.labels.map(label => (
<ListCardLabel color={label.projectLabel.labelColor.colorHex} key={label.id}>
{label.projectLabel.name}
</ListCardLabel>
))}
</ListCardLabels>
<EditorDetails>
<EditorTextarea
onChange={e => setCardTitle(e.currentTarget.value)}
onClick={e => {
e.stopPropagation();
<Card
editable
onCardMemberClick={onCardMemberClick}
title={currentCardTitle}
onEditCard={(taskGroupID, taskID, name) => {
onEditCard(taskGroupID, taskID, name);
onCloseEditor();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
members={task.assigned}
taskID={task.id}
taskGroupID={task.taskGroup.id}
labels={task.labels.map(l => l.projectLabel)}
/>
<CardMembers>
{task.assigned &&
task.assigned.map(member => (
<Member key={member.id} taskID={task.id} member={member} onCardMemberClick={onCardMemberClick} />
))}
</CardMembers>
</EditorDetails>
</Editor>
<SaveButton onClick={e => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton>
<SaveButton onClick={() => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton>
<EditorButtons>
<EditorButton
ref={$membersRef}

View File

@ -1,67 +1,8 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User } from 'shared/icons';
const TextFieldWrapper = styled.div`
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
justify-content: center;
margin-bottom: 2.2rem;
margin-top: 17px;
`;
const TextFieldLabel = styled.span`
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;
width: 100%;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const TextFieldInput = styled.input`
font-size: 12px;
border: 1px solid rgba(0, 0, 0, 0.2);
background: #262c49;
padding: 0.7rem !important;
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
width: 100%;
&:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240);
}
&:focus ~ ${TextFieldLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
`;
type TextFieldProps = {
label: string;
};
const TextField: React.FC<TextFieldProps> = ({ label }) => {
return (
<TextFieldWrapper>
<TextFieldInput />
<TextFieldLabel>{label}</TextFieldLabel>
</TextFieldWrapper>
);
};
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<AvatarSettingsProps> = ({ profile, onProfileAvata
</AvatarContainer>
<ActionButtons>
<UploadButton onClick={() => onProfileAvatarChange()}>Upload photo</UploadButton>
<RemoveButton onClick={() => onProfileAvatarRemove()}>Remove</RemoveButton>
<RemoveButton variant="outline" color="danger" onClick={() => onProfileAvatarRemove()}>
Remove
</RemoveButton>
<ImgLabel>Allowed JPG, GIF or PNG. Max size of 800kB</ImgLabel>
</ActionButtons>
</ProfileContainer>
@ -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<SettingsProps> = ({ onProfileAvatarRemove, onProfileAva
onProfileAvatarChange={onProfileAvatarChange}
profile={profile}
/>
<TextField label="Name" />
<TextField label="Initials " />
<TextField label="Username " />
<TextField label="Email" />
<TextField label="Bio" />
<Input width="100%" label="Name" />
<Input width="100%" label="Initials " />
<Input width="100%" label="Username " />
<Input width="100%" label="Email" />
<Input width="100%" label="Bio" />
<SettingActions>
<SaveButton>Save Change</SaveButton>
</SettingActions>

View File

@ -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 = {

View File

@ -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`

View File

@ -88,7 +88,9 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
/>
<TaskDetailsControls>
<ConfirmSave onClick={handleOutsideClick}>Save</ConfirmSave>
<ConfirmSave variant="relief" onClick={handleOutsideClick}>
Save
</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Plus size={16} color="#c2c6dc" />
</CancelEdit>

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

@ -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;
`;

View File

@ -176,7 +176,7 @@ const NavBar: React.FC<NavBarProps> = ({
{projectMembers.map(member => (
<TaskAssignee key={member.id} size={28} member={member} onMemberProfile={onMemberProfile} />
))}
<InviteButton>Invite</InviteButton>
<InviteButton variant="outline">Invite</InviteButton>
</ProjectMembers>
)}
<NotificationContainer onClick={onNotificationClick}>

30
web/src/styled.d.ts vendored Normal file
View File

@ -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;
};
};
}
}