chore: add theme colors, merge Card components, create single Button component
This commit is contained in:
parent
6267a37b6e
commit
a12e9c1e50
@ -70,7 +70,7 @@ export default createGlobalStyle`
|
||||
outline: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
[role="button"], button, input, textarea {
|
||||
|
47
web/src/App/ThemeStyles.ts
Normal file
47
web/src/App/ThemeStyles.ts
Normal 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);
|
||||
}
|
||||
`;
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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 = [];
|
||||
|
@ -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`
|
||||
|
@ -50,6 +50,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
||||
</ListNameEditorWrapper>
|
||||
<ListAddControls>
|
||||
<AddListButton
|
||||
variant="relief"
|
||||
onClick={() => {
|
||||
onSave(listName);
|
||||
setListName('');
|
||||
|
138
web/src/shared/components/Button/Button.stories.tsx
Normal file
138
web/src/shared/components/Button/Button.stories.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
171
web/src/shared/components/Button/index.tsx
Normal file
171
web/src/shared/components/Button/index.tsx
Normal 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;
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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 />
|
||||
|
43
web/src/shared/components/Input/Input.stories.tsx
Normal file
43
web/src/shared/components/Input/Input.stories.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
91
web/src/shared/components/Input/index.tsx
Normal file
91
web/src/shared/components/Input/index.tsx
Normal 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;
|
@ -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={[]}
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -25,6 +25,7 @@ const CardMemberInitials = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
type MemberProps = {
|
||||
|
@ -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);
|
||||
|
@ -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};
|
||||
`;
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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`
|
||||
|
@ -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>
|
||||
|
31
web/src/shared/components/Textarea/index.tsx
Normal file
31
web/src/shared/components/Textarea/index.tsx
Normal 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;
|
@ -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;
|
||||
`;
|
||||
|
@ -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
30
web/src/styled.d.ts
vendored
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user