1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 21:06:42 +07:00

notification menu

This commit is contained in:
Fedor Katurov 2019-11-13 12:11:47 +07:00
parent 6c1f8967e8
commit 9b0c3dd1fb
20 changed files with 371 additions and 39 deletions

View file

@ -13,6 +13,8 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { DIALOGS } from '~/redux/modal/constants';
import { pick } from 'ramda';
import { UserButton } from '../UserButton';
import { Icon } from '~/components/input/Icon';
import { Notifications } from '../Notifications';
const mapStateToProps = state => ({
user: pick(['username', 'is_user', 'photo'])(selectUser(state)),
@ -39,8 +41,7 @@ const HeaderUnconnected: FC<IProps> = memo(
<Filler />
<div className={style.plugs}>
<Link to="/boris">((( boris )))</Link>
<Link to="/">flow</Link>
<Notifications />
</div>
{is_user && (

View file

@ -14,11 +14,24 @@
.plugs {
display: flex;
user-select: none;
font: $font_16_medium;
text-transform: uppercase;
align-items: center;
> a,
> div {
&::after {
content: ' ';
margin-left: $spc;
background: white;
width: 4px;
height: $gap;
display: block;
opacity: 0.2;
border-radius: 4px;
margin-right: 10px;
}
& > a.item,
& > div.item {
font: $font_16_medium;
display: flex;
align-items: center;
position: relative;
@ -33,17 +46,6 @@
color: $red;
}
&::after {
content: ' ';
margin-left: $spc;
background: white;
width: 4px;
height: $gap;
display: block;
opacity: 0.2;
border-radius: 4px;
}
&:first-child {
padding-left: $spc + $gap;
}

View file

@ -1,8 +1,9 @@
import * as React from 'react';
import * as styles from './style.scss';
import React from 'react';
import styles from './style.scss';
import { Link } from 'react-router-dom';
export const Logo = () => (
<div className={styles.logo}>
<Link className={styles.logo} to="/">
VAULT
</div>
</Link>
);

View file

@ -1,9 +1,7 @@
.logo {
// font-family: Raleway;
//font-size: $text_sign;
//font-weight: 800;
font: $font_24_bold;
//font-family: Raleway;
display: flex;
user-select: none;
color: white;
text-decoration: none;
}

View file

@ -0,0 +1,62 @@
import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
import { Icon } from '~/components/input/Icon';
import styles from './styles.scss';
import { connect } from 'react-redux';
import { selectAuthUpdates, selectAuthUser } from '~/redux/auth/selectors';
import pick from 'ramda/es/pick';
import classNames from 'classnames';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { NotificationBubble } from '../../notifications/NotificationBubble';
const mapStateToProps = state => ({
user: pick(['last_seen_messages'], selectAuthUser(state)),
updates: selectAuthUpdates(state),
});
const mapDispatchToProps = {
authSetLastSeenMessages: AUTH_ACTIONS.authSetLastSeenMessages,
};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const NotificationsUnconnected: FC<IProps> = ({
updates: { last, notifications },
user: { last_seen_messages },
authSetLastSeenMessages,
}) => {
const [visible, setVisible] = useState(true);
const has_new = useMemo(
() =>
notifications.length &&
last &&
Date.parse(last) &&
(!last_seen_messages ||
(Date.parse(last_seen_messages) && Date.parse(last) > Date.parse(last_seen_messages))),
[last, last_seen_messages, notifications]
);
useEffect(() => {
if (!visible || !has_new) return;
authSetLastSeenMessages(new Date().toISOString());
}, [visible]);
const showList = useCallback(() => setVisible(true), [setVisible]);
const hideList = useCallback(() => setVisible(false), [setVisible]);
return (
<div className={classNames(styles.wrap, { [styles.is_new]: has_new })}>
<div className={styles.icon} onFocus={showList} onBlur={hideList} tabIndex={-1}>
{has_new ? <Icon icon="bell_ring" size={24} /> : <Icon icon="bell" size={24} />}
</div>
{visible && <NotificationBubble notifications={notifications} />}
</div>
);
};
const Notifications = connect(
mapStateToProps,
mapDispatchToProps
)(NotificationsUnconnected);
export { Notifications };

View file

@ -0,0 +1,38 @@
@keyframes ring {
0% {
transform: rotate(-10deg);
}
20% {
transform: rotate(10deg);
}
40% {
transform: rotate(-10deg);
}
100% {
transform: rotate(0);
}
}
.wrap {
fill: white;
position: relative;
outline: none;
&.is_new {
.icon {
animation: ring 1s infinite alternate;
svg {
fill: $red;
}
}
}
}
.icon {
outline: none;
cursor: pointer;
}

View file

@ -0,0 +1,31 @@
import React, { FC, createElement } from 'react';
import { INotification, NOTIFICATION_TYPES } from '~/redux/types';
import styles from './styles.scss';
import { NotificationMessage } from '../NotificationMessage';
interface IProps {
notifications: INotification[];
}
const NOTIFICATION_RENDERERS = {
[NOTIFICATION_TYPES.message]: NotificationMessage,
};
const NotificationBubble: FC<IProps> = ({ notifications }) => {
return (
<div className={styles.wrap}>
<div className={styles.list}>
{notifications
.filter(notification => notification.type && NOTIFICATION_RENDERERS[notification.type])
.map(notification =>
createElement(NOTIFICATION_RENDERERS[notification.type], {
notification,
key: notification.content.id,
})
)}
</div>
</div>
);
};
export { NotificationBubble };

View file

@ -0,0 +1,76 @@
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.wrap {
position: absolute;
position: absolute;
background: $content_bg;
top: 50px;
left: 50%;
transform: translate(-50%, 0);
border-radius: $radius;
animation: appear 0.5s forwards;
&::before {
content: ' ';
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 16px 16px;
border-color: transparent transparent $content_bg transparent;
position: absolute;
left: 50%;
top: -16px;
transform: translate(-20px, 0);
}
}
.list {
width: 300px;
max-width: 100vw;
min-width: 0;
max-height: 400px;
overflow: auto;
}
.item {
display: flex;
align-items: stretch;
justify-content: stretch;
flex-direction: column;
padding: $gap;
min-width: 0;
svg {
fill: white;
margin-right: $gap;
}
}
.item_head {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
}
.item_title {
flex: 1;
white-space: nowrap;
font: $font_14_semibold;
overflow: hidden;
text-overflow: ellipsis;
// text-transform: none;
}
.item_text {
font: $font_14_regular;
max-height: 2.4em;
padding-left: 30px;
overflow: hidden;
}

View file

@ -0,0 +1,26 @@
import React, { FC } from 'react';
import styles from '~/components/notifications/NotificationBubble/styles.scss';
import { Icon } from '~/components/input/Icon';
import { IMessageNotification } from '~/redux/types';
interface IProps {
notification: IMessageNotification;
}
const NotificationMessage: FC<IProps> = ({
notification: {
content: { text, from },
},
}) => {
return (
<div className={styles.item}>
<div className={styles.item_head}>
<Icon icon="message" />
<div className={styles.item_title}>Сообщение от ~{from.username}:</div>
</div>
<div className={styles.item_text}>{text}</div>
</div>
);
};
export { NotificationMessage };