arch: move web folder into api & move api to top level
This commit is contained in:
174
frontend/src/shared/components/Card/Card.stories.tsx
Normal file
174
frontend/src/shared/components/Card/Card.stories.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import Card from '.';
|
||||
|
||||
export default {
|
||||
component: Card,
|
||||
title: 'Card',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'gray', value: '#f8f8f8', default: true },
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const labelData: Array<ProjectLabel> = [
|
||||
{
|
||||
id: 'development',
|
||||
name: 'Development',
|
||||
createdDate: new Date().toString(),
|
||||
labelColor: {
|
||||
id: '1',
|
||||
colorHex: LabelColors.BLUE,
|
||||
name: 'blue',
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
taskID="1"
|
||||
taskGroupID="1"
|
||||
description=""
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Labels = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
taskID="1"
|
||||
taskGroupID="1"
|
||||
description=""
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
labels={labelData}
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Badges = () => {
|
||||
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
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PastDue = () => {
|
||||
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
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: true, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Everything = () => {
|
||||
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
|
||||
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')}
|
||||
/>
|
||||
);
|
||||
};
|
171
frontend/src/shared/components/Card/Styles.ts
Normal file
171
frontend/src/shared/components/Card/Styles.ts
Normal file
@ -0,0 +1,171 @@
|
||||
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 { CheckCircle } from 'shared/icons';
|
||||
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: 14px;
|
||||
line-height: 16px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ListCardBadges = styled.div`
|
||||
float: left;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin-left: -2px;
|
||||
`;
|
||||
|
||||
export const ListCardBadge = styled.div`
|
||||
color: #5e6c84;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 6px 4px 0;
|
||||
font-size: 12px;
|
||||
max-width: 100%;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 2px;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
`;
|
||||
|
||||
export const DescriptionBadge = styled(ListCardBadge)`
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||
font-size: 12px;
|
||||
${props =>
|
||||
props.isPastDue &&
|
||||
css`
|
||||
padding-left: 4px;
|
||||
background-color: #ec9488;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ListCardBadgeText = styled.span`
|
||||
font-size: 12px;
|
||||
padding: 0 4px 0 6px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
|
||||
max-width: 256px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 3px;
|
||||
${mixin.boxShadowCard}
|
||||
cursor: pointer !important;
|
||||
position: relative;
|
||||
|
||||
background-color: ${props =>
|
||||
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`};
|
||||
`;
|
||||
|
||||
export const ListCardInnerContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const ListCardDetails = styled.div<{ complete: boolean }>`
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
${props => props.complete && 'opacity: 0.6;'}
|
||||
`;
|
||||
|
||||
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};
|
||||
`;
|
||||
|
||||
export const ListCardOperation = styled.span`
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
opacity: 0.8;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props => mixin.darken('#262c49', 0.45)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardTitle = styled.span`
|
||||
clear: both;
|
||||
margin: 0 0 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
line-height: 16px;
|
||||
font-size: 14px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const CardMembers = styled.div`
|
||||
float: right;
|
||||
margin: 0 -2px 4px 0;
|
||||
`;
|
||||
|
||||
export const CompleteIcon = styled(CheckCircle)`
|
||||
fill: rgba(${props => props.theme.colors.success});
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const EditorContent = styled.div`
|
||||
display: flex;
|
||||
`;
|
220
frontend/src/shared/components/Card/index.tsx
Normal file
220
frontend/src/shared/components/Card/index.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
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,
|
||||
EditorContent,
|
||||
CompleteIcon,
|
||||
DescriptionBadge,
|
||||
DueDateCardBadge,
|
||||
ListCardBadges,
|
||||
ListCardBadge,
|
||||
ListCardBadgeText,
|
||||
ListCardContainer,
|
||||
ListCardInnerContainer,
|
||||
ListCardDetails,
|
||||
ClockIcon,
|
||||
ListCardLabels,
|
||||
ListCardLabel,
|
||||
ListCardOperation,
|
||||
CardTitle,
|
||||
CardMembers,
|
||||
} from './Styles';
|
||||
|
||||
type DueDate = {
|
||||
isPastDue: boolean;
|
||||
formattedDate: string;
|
||||
};
|
||||
|
||||
type Checklist = {
|
||||
complete: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
taskID: string;
|
||||
taskGroupID: string;
|
||||
complete?: boolean;
|
||||
onContextMenu?: ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
description?: null | string;
|
||||
dueDate?: DueDate;
|
||||
checklists?: Checklist | null;
|
||||
labels?: Array<ProjectLabel>;
|
||||
watched?: boolean;
|
||||
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(
|
||||
(
|
||||
{
|
||||
wrapperProps,
|
||||
onContextMenu,
|
||||
taskID,
|
||||
taskGroupID,
|
||||
complete,
|
||||
onClick,
|
||||
labels,
|
||||
title,
|
||||
dueDate,
|
||||
description,
|
||||
checklists,
|
||||
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 (onContextMenu) {
|
||||
onContextMenu($innerCardRef, taskID, taskGroupID);
|
||||
}
|
||||
};
|
||||
const onTaskContext = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenComposer();
|
||||
};
|
||||
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenComposer();
|
||||
};
|
||||
return (
|
||||
<ListCardContainer
|
||||
onMouseEnter={() => setActive(true)}
|
||||
onMouseLeave={() => setActive(false)}
|
||||
ref={$cardRef}
|
||||
onClick={e => {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
onContextMenu={onTaskContext}
|
||||
isActive={isActive}
|
||||
editable={editable}
|
||||
{...wrapperProps}
|
||||
>
|
||||
<ListCardInnerContainer ref={$innerCardRef}>
|
||||
{isActive && (
|
||||
<ListCardOperation>
|
||||
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
|
||||
</ListCardOperation>
|
||||
)}
|
||||
<ListCardDetails complete={complete ?? false}>
|
||||
<ListCardLabels>
|
||||
{labels &&
|
||||
labels.map(label => (
|
||||
<ListCardLabel color={label.labelColor.colorHex} key={label.id}>
|
||||
{label.name}
|
||||
</ListCardLabel>
|
||||
))}
|
||||
</ListCardLabels>
|
||||
{editable ? (
|
||||
<EditorContent>
|
||||
{complete && <CompleteIcon width={16} height={16} />}
|
||||
<EditorTextarea
|
||||
onChange={e => {
|
||||
setCardTitle(e.currentTarget.value);
|
||||
if (onCardTitleChange) {
|
||||
onCardTitleChange(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={currentCardTitle}
|
||||
ref={$editorRef}
|
||||
/>
|
||||
</EditorContent>
|
||||
) : (
|
||||
<CardTitle>
|
||||
{complete && <CompleteIcon width={16} height={16} />}
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<ListCardBadges>
|
||||
{watched && (
|
||||
<ListCardBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
|
||||
</ListCardBadge>
|
||||
)}
|
||||
{dueDate && (
|
||||
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
|
||||
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
|
||||
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
|
||||
</DueDateCardBadge>
|
||||
)}
|
||||
{description && (
|
||||
<DescriptionBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
|
||||
</DescriptionBadge>
|
||||
)}
|
||||
{checklists && (
|
||||
<ListCardBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faCheckSquare} size="xs" />
|
||||
<ListCardBadgeText>{`${checklists.complete}/${checklists.total}`}</ListCardBadgeText>
|
||||
</ListCardBadge>
|
||||
)}
|
||||
</ListCardBadges>
|
||||
<CardMembers>
|
||||
{members &&
|
||||
members.map(member => (
|
||||
<TaskAssignee
|
||||
key={member.id}
|
||||
size={28}
|
||||
member={member}
|
||||
onMemberProfile={$target => {
|
||||
if (onCardMemberClick) {
|
||||
onCardMemberClick($target, taskID, member.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CardMembers>
|
||||
</ListCardDetails>
|
||||
</ListCardInnerContainer>
|
||||
</ListCardContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export default Card;
|
Reference in New Issue
Block a user