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,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Icon, Menu } from 'semantic-ui-react';
import Paths from '../../constants/Paths';
import NotificationsPopup from './NotificationsPopup';
import UserPopup from './UserPopup';
import styles from './Header.module.css';
const Header = React.memo(
({
user,
notifications,
isEditable,
onUserUpdate,
onUserAvatarUpload,
onNotificationDelete,
onUsers,
onLogout,
}) => {
const [t] = useTranslation();
return (
<div className={styles.wrapper}>
<Link to={Paths.ROOT} className={styles.logo}>
Planka
</Link>
<Menu inverted size="large" className={styles.menu}>
{isEditable && (
<Menu.Item className={styles.item} onClick={onUsers}>
{t('common.users', {
context: 'title',
})}
</Menu.Item>
)}
<Menu.Menu position="right">
<NotificationsPopup items={notifications} onDelete={onNotificationDelete}>
<Menu.Item className={styles.item}>
<Icon name="bell" className={styles.icon} />
{notifications.length > 0 && (
<span className={styles.notification}>{notifications.length}</span>
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup
name={user.name}
avatar={user.avatar}
isAvatarUploading={user.isAvatarUploading}
onUpdate={onUserUpdate}
onAvatarUpload={onUserAvatarUpload}
onLogout={onLogout}
>
<Menu.Item className={styles.item}>{user.name}</Menu.Item>
</UserPopup>
</Menu.Menu>
</Menu>
</div>
);
},
);
Header.propTypes = {
/* eslint-disable react/forbid-prop-types */
user: PropTypes.object.isRequired,
notifications: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
onUserUpdate: PropTypes.func.isRequired,
onUserAvatarUpload: PropTypes.func.isRequired,
onNotificationDelete: PropTypes.func.isRequired,
onUsers: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
};
export default Header;

View File

@@ -0,0 +1,58 @@
.icon {
margin: 0 !important;
}
.item:before {
background: none !important;
}
.logo {
background: #2d3035;
color: #fff !important;
flex: 0 0 auto;
font-size: 20px;
font-weight: bold;
letter-spacing: 3.5px;
line-height: 50px;
padding: 0 16px;
text-transform: uppercase;
width: 130px;
}
.logo:before {
background: none !important;
}
.menu {
background: #2d3035 !important;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
color: #fff !important;
flex: 1 1 auto;
height: 50px;
margin: 0 !important;
width: 100%;
z-index: 1;
}
.notification {
background: #eb5a46;
border-radius: 8px;
color: #fff;
display: inline-block;
font-size: 14px;
font-weight: bold;
height: 16px;
line-height: 16px;
position: absolute;
right: 8px;
text-align: center;
top: 8px;
width: 16px;
}
.wrapper {
display: flex;
flex: 0 0 auto;
}

View File

@@ -0,0 +1,114 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Button } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
import Paths from '../../constants/Paths';
import { ActionTypes } from '../../constants/Enums';
import User from '../User';
import styles from './NotificationsPopup.module.css';
const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
const [t] = useTranslation();
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const renderItemContent = useCallback(
({ action, card }) => {
switch (action.type) {
case ActionTypes.MOVE_CARD:
return (
<Trans
i18nKey="common.userMovedCardFromListToList"
values={{
user: action.user.name,
card: card.name,
fromList: action.data.fromList.name,
toList: action.data.toList.name,
}}
>
{action.user.name}
{' moved '}
<Link to={Paths.CARDS.replace(':id', card.id)} onClick={onClose}>
{card.name}
</Link>
{' from '}
{action.data.fromList.name}
{' to '}
{action.data.toList.name}
</Trans>
);
case ActionTypes.COMMENT_CARD:
return (
<Trans
i18nKey="common.userLeftNewCommentToCard"
values={{
user: action.user.name,
comment: action.data.text,
card: card.name,
}}
>
{action.user.name}
{` left a new comment «${action.data.text}» to `}
<Link to={Paths.CARDS.replace(':id', card.id)} onClick={onClose}>
{card.name}
</Link>
</Trans>
);
default:
}
return null;
},
[onClose],
);
return (
<>
<Popup.Header>{t('common.notifications')}</Popup.Header>
<Popup.Content>
{items.length > 0
? items.map((item) => (
<div key={item.id} className={styles.wrapper}>
{item.card && item.action ? (
<>
<User
name={item.action.user.name}
avatar={item.action.user.avatar}
size="large"
/>
<span className={styles.content}>{renderItemContent(item)}</span>
</>
) : (
<div className={styles.deletedContent}>{t('common.cardOrActionAreDeleted')}</div>
)}
<Button
type="button"
icon="close"
className={styles.button}
onClick={() => handleDelete(item.id)}
/>
</div>
))
: t('common.noUnreadNotifications')}
</Popup.Content>
</>
);
});
NotificationsStep.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(NotificationsStep);

View File

@@ -0,0 +1,45 @@
.button {
background: transparent !important;
box-shadow: none !important;
float: right !important;
height: 20px !important;
line-height: 20px !important;
margin: 0 !important;
min-height: auto !important;
padding: 5px 0 !important;
transition: background 0.3s ease !important;
width: 20px !important;
}
.button:hover {
background: #e9e9e9 !important;
}
.content {
display: inline-block !important;
font-size: 12px;
min-height: 36px !important;
overflow: hidden;
padding: 0 4px 0 8px !important;
vertical-align: top !important;
width: calc(100% - 56px) !important;
word-break: break-word;
}
.deletedContent {
display: inline-block !important;
line-height: 20px !important;
min-height: 20px !important;
padding: 0 4px 0 8px !important;
vertical-align: top !important;
width: calc(100% - 20px) !important;
}
.wrapper {
margin: 0 -12px !important;
padding: 12px !important;
}
.wrapper:hover {
background: #f0f0f0 !important;
}

View File

@@ -0,0 +1,74 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../../lib/custom-ui';
import User from '../../User';
import styles from './EditAvatarStep.module.css';
const EditAvatarStep = React.memo(
({
defaultValue, name, isUploading, onUpload, onClear, onBack,
}) => {
const [t] = useTranslation();
const field = useRef(null);
const handleFieldChange = useCallback(
({ target }) => {
if (target.files[0]) {
onUpload(target.files[0]);
target.value = null; // eslint-disable-line no-param-reassign
}
},
[onUpload],
);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editAvatar', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<User name={name} avatar={defaultValue} size="large" />
<div className={styles.input}>
<Button content={t('action.uploadNewAvatar')} className={styles.customButton} />
<input
ref={field}
type="file"
accept="image/*"
disabled={isUploading}
className={styles.file}
onChange={handleFieldChange}
/>
</div>
{defaultValue && <Button negative content={t('action.deleteAvatar')} onClick={onClear} />}
</Popup.Content>
</>
);
},
);
EditAvatarStep.propTypes = {
defaultValue: PropTypes.string,
name: PropTypes.string.isRequired,
isUploading: PropTypes.bool.isRequired,
onUpload: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
EditAvatarStep.defaultProps = {
defaultValue: undefined,
};
export default EditAvatarStep;

View File

@@ -0,0 +1,35 @@
.customButton {
background: transparent !important;
color: #6b808c !important;
font-weight: normal !important;
height: 36px;
line-height: 24px !important;
padding: 6px 12px !important;
text-align: left !important;
text-decoration: underline !important;
}
.file {
bottom: 0;
left: 0;
opacity: 0;
position: absolute;
right: 0;
top: 0;
z-index: 1;
}
.input {
border: none;
display: inline-block;
height: 36px;
overflow: hidden;
margin-left: 8px;
position: relative;
transition: background 0.3s ease;
width: calc(100% - 44px);
}
.input:hover {
background: #e9e9e9 !important;
}

View File

@@ -0,0 +1,68 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { Input, Popup } from '../../../lib/custom-ui';
import { useField } from '../../../hooks';
import styles from './EditNameStep.module.css';
const EditNameStep = React.memo(({
defaultValue, onUpdate, onBack, onClose,
}) => {
const [t] = useTranslation();
const [value, handleFieldChange] = useField(defaultValue);
const field = useRef(null);
const handleSubmit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
onClose();
}, [defaultValue, onUpdate, onClose, value]);
useEffect(() => {
field.current.select();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editName', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Input
fluid
ref={field}
value={value}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
</Popup.Content>
</>
);
});
EditNameStep.propTypes = {
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default EditNameStep;

View File

@@ -0,0 +1,3 @@
.field {
margin-bottom: 8px;
}

View File

@@ -0,0 +1,116 @@
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 EditNameStep from './EditNameStep';
import EditAvatarStep from './EditAvatarStep';
import styles from './UserPopup.module.css';
const StepTypes = {
EDIT_NAME: 'EDIT_NAME',
EDIT_AVATAR: 'EDIT_AVATAR',
};
const UserStep = React.memo(
({
name, avatar, isAvatarUploading, onUpdate, onAvatarUpload, onLogout, onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => {
openStep(StepTypes.EDIT_NAME);
}, [openStep]);
const handleAvatarEditClick = useCallback(() => {
openStep(StepTypes.EDIT_AVATAR);
}, [openStep]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleAvatarClear = useCallback(() => {
onUpdate({
avatar: null,
});
}, [onUpdate]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_NAME:
return (
<EditNameStep
defaultValue={name}
onUpdate={handleNameUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_AVATAR:
return (
<EditAvatarStep
defaultValue={avatar}
name={name}
isUploading={isAvatarUploading}
onUpload={onAvatarUpload}
onClear={handleAvatarClear}
onBack={handleBack}
/>
);
default:
}
}
return (
<>
<Popup.Header>{name}</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
{t('action.editName', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleAvatarEditClick}>
{t('action.editAvatar', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={onLogout}>
{t('action.logOut', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
UserStep.propTypes = {
name: PropTypes.string.isRequired,
avatar: PropTypes.string,
isAvatarUploading: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onAvatarUpload: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
UserStep.defaultProps = {
avatar: undefined,
};
export default withPopup(UserStep);

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,3 @@
import UserPopup from './UserPopup';
export default UserPopup;

View File

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