Initial commit

This commit is contained in:
Maksim Eltyshev
2019-08-31 04:07:25 +05:00
commit 5ffef61fe7
613 changed files with 91659 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.css';
const StepTypes = {
DELETE: 'DELETE',
};
const ActionsStep = React.memo(({
onNameEdit, onCardAdd, onDelete, onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleCardAddClick = useCallback(() => {
onCardAdd();
onClose();
}, [onCardAdd, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteList', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisList')}
buttonContent={t('action.deleteList')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header>
{t('common.listActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleCardAddClick}>
{t('action.addCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View File

@@ -0,0 +1,9 @@
.menu {
margin: -7px -12px -5px !important;
width: calc(100% + 24px) !important;
}
.menuItem {
margin: 0 !important;
padding-left: 14px !important;
}

View File

@@ -0,0 +1,149 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import {
useClosableForm,
useDeepCompareCallback,
useDidUpdate,
useForm,
useToggle,
} from '../../hooks';
import styles from './AddCard.module.css';
const DEFAULT_DATA = {
name: '',
};
const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
const submit = useDeepCompareCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.ref.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
selectNameField();
}, [onCreate, data, setData, selectNameField]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback(
(event) => {
switch (event.key) {
case 'Enter':
event.preventDefault();
submit();
break;
case 'Escape':
close();
break;
default:
}
},
[close, submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
nameField.current.ref.current.select();
}
}, [isOpened]);
useDidUpdate(() => {
nameField.current.ref.current.select();
}, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<div className={styles.fieldWrapper}>
<TextArea
ref={nameField}
as={TextareaAutosize}
name="name"
value={data.name}
placeholder={t('common.enterCardTitle')}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
</div>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.addCard')}
className={styles.submitButton}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
AddCard.propTypes = {
children: PropTypes.element.isRequired,
onCreate: PropTypes.func.isRequired,
};
export default React.memo(AddCard);

View File

@@ -0,0 +1,26 @@
.field {
border: none !important;
margin-bottom: 4px !important;
outline: none !important;
overflow: hidden !important;
padding: 0 !important;
resize: none !important;
width: 100% !important;
}
.fieldWrapper {
background-color: #fff !important;
border-radius: 3px !important;
box-shadow: 0 1px 0 #ccc !important;
margin-bottom: 8px !important;
min-height: 20px !important;
padding: 6px 8px 2px !important;
}
.submitButton {
vertical-align: top !important;
}
.wrapper {
padding-bottom: 8px !important;
}

View File

@@ -0,0 +1,105 @@
import React, {
useCallback, useEffect, useImperativeHandle, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import TextareaAutosize from 'react-textarea-autosize';
import { TextArea } from 'semantic-ui-react';
import { useField } from '../../hooks';
import styles from './EditName.module.css';
const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) => {
const [isOpened, setIsOpened] = useState(false);
const [value, handleFieldChange, setValue] = useField(defaultValue);
const field = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
setValue(defaultValue);
}, [defaultValue, setValue]);
const close = useCallback(() => {
setIsOpened(false);
setValue(null);
}, [setValue]);
const submit = useCallback(() => {
const cleanValue = value.trim();
if (cleanValue && cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
close();
}, [defaultValue, onUpdate, value, close]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleFieldClick = useCallback((event) => {
event.stopPropagation();
}, []);
const handleFieldKeyDown = useCallback(
(event) => {
switch (event.key) {
case 'Enter':
event.preventDefault();
submit();
break;
case 'Escape':
submit();
break;
default:
}
},
[submit],
);
const handleFieldBlur = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
field.current.ref.current.select();
}
}, [isOpened]);
if (!isOpened) {
return children;
}
return (
<TextArea
ref={field}
as={TextareaAutosize}
value={value}
spellCheck={false}
className={styles.field}
onClick={handleFieldClick}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
);
});
EditName.propTypes = {
children: PropTypes.element.isRequired,
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(EditName);

View File

@@ -0,0 +1,22 @@
.field {
border: none;
border-color: #bbb !important;
border-radius: 3px !important;
color: #17394d !important;
display: block !important;
font-weight: bold !important;
line-height: 20px !important;
margin: 0 !important;
outline: none !important;
overflow: hidden !important;
padding: 4px 8px !important;
resize: none !important;
/* word-break: break-all !important; */
width: 100% !important;
}
.field:focus {
background: #fff !important;
border-color: #5ba4cf !important;
box-shadow: 0 0 0 1px #5ba4cf !important;
}

View File

@@ -0,0 +1,123 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import DroppableTypes from '../../constants/DroppableTypes';
import CardContainer from '../../containers/CardContainer';
import EditName from './EditName';
import AddCard from './AddCard';
import ActionsPopup from './ActionsPopup';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './List.module.css';
const List = React.memo(
({
id, index, name, isPersisted, cardIds, onUpdate, onDelete, onCardCreate,
}) => {
const [t] = useTranslation();
const addCard = useRef(null);
const editName = useRef(null);
const handleHeaderClick = useCallback(() => {
if (isPersisted) {
editName.current.open();
}
}, [isPersisted]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleNameEdit = useCallback(() => {
editName.current.open();
}, []);
const handleCardAdd = useCallback(() => {
addCard.current.open();
}, []);
const cardsNode = (
<Droppable
droppableId={`list:${id}`}
type={DroppableTypes.CARD}
isDropDisabled={!isPersisted}
>
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef}>
<div className={styles.cards}>
{cardIds.map((cardId, cardIndex) => (
<CardContainer key={cardId} id={cardId} index={cardIndex} />
))}
{placeholder}
</div>
<AddCard ref={addCard} onCreate={onCardCreate}>
<button type="button" disabled={!isPersisted} className={styles.addCardButton}>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</span>
</button>
</AddCard>
</div>
)}
</Droppable>
);
return (
<Draggable draggableId={`list:${id}`} index={index} isDragDisabled={!isPersisted}>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} data-drag-scroller ref={innerRef} className={styles.wrapper}>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<div {...dragHandleProps} className={styles.header} onClick={handleHeaderClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<EditName ref={editName} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.headerName}>{name}</div>
</EditName>
{isPersisted && (
<ActionsPopup
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</div>
<div className={styles.list}>{cardsNode}</div>
</div>
)}
</Draggable>
);
},
);
List.propTypes = {
id: PropTypes.number.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired,
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};
export default List;

View File

@@ -0,0 +1,113 @@
.addCardButton {
background: none !important;
border: none !important;
color: #6b808c !important;
cursor: pointer;
display: block !important;
fill: #6b808c !important;
flex: 0 0 auto;
font-weight: normal !important;
height: 36px !important;
margin: 0 -8px !important;
padding: 8px !important;
text-align: left !important;
width: calc(100% + 16px);
}
.addCardButton:active {
outline: none !important;
}
.addCardButton:hover {
background-color: #092d4221 !important;
color: #17394d !important;
fill: #17394d !important;
}
.addCardButtonIcon {
height: 20px !important;
padding: 0.64px !important;
width: 20px !important;
}
.addCardButtonText {
display: inline-block !important;
font-size: 14px !important;
line-height: 20px !important;
margin-left: 2px;
vertical-align: top !important;
}
.cards {
flex: 1 1 auto;
}
.header {
background: #dfe3e6 !important;
border-radius: 3px 3px 0 0 !important;
box-sizing: none;
flex: 0 0 auto;
margin-bottom: 0 !important;
outline: none !important;
padding: 6px 36px 4px 8px !important;
position: relative !important;
}
.header:hover .target {
opacity: 1 !important;
}
.headerButton {
background: none !important;
box-shadow: none !important;
color: #798d99 !important;
line-height: 32px !important;
margin: 0 !important;
opacity: 0;
padding: 0 !important;
position: absolute;
right: 2px;
top: 2px;
width: 32px;
}
.headerButton:hover {
background-color: rgba(9, 45, 66, 0.13) !important;
color: #516b7a !important;
}
.headerName {
background: transparent !important;
border-color: transparent !important;
border-radius: 3px !important;
color: #17394d !important;
font-weight: bold !important;
line-height: 20px !important;
margin: 0 !important;
max-height: 256px !important;
outline: none !important;
overflow: hidden !important;
overflow-wrap: break-word !important;
padding: 4px 8px !important;
resize: none !important;
width: 100% !important;
word-break: break-word !important;
}
.list {
background: #dfe3e6 !important;
border-radius: 0 0 3px 3px !important;
box-sizing: border-box !important;
display: flex;
flex-direction: column;
padding: 0 8px !important;
position: relative !important;
white-space: normal !important;
}
.wrapper {
flex: 0 0 auto;
margin: 0 4px !important;
vertical-align: top !important;
width: 272px !important;
}

View File

@@ -0,0 +1,3 @@
import List from './List';
export default List;