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:
parent
6c1f8967e8
commit
9b0c3dd1fb
20 changed files with 371 additions and 39 deletions
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
62
src/components/main/Notifications/index.tsx
Normal file
62
src/components/main/Notifications/index.tsx
Normal 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 };
|
38
src/components/main/Notifications/styles.scss
Normal file
38
src/components/main/Notifications/styles.scss
Normal 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;
|
||||
}
|
31
src/components/notifications/NotificationBubble/index.tsx
Normal file
31
src/components/notifications/NotificationBubble/index.tsx
Normal 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 };
|
76
src/components/notifications/NotificationBubble/styles.scss
Normal file
76
src/components/notifications/NotificationBubble/styles.scss
Normal 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;
|
||||
}
|
26
src/components/notifications/NotificationMessage/index.tsx
Normal file
26
src/components/notifications/NotificationMessage/index.tsx
Normal 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 };
|
Loading…
Add table
Add a link
Reference in a new issue