mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
notifications: added profile indicator
This commit is contained in:
parent
97590d88af
commit
dc90f2505c
10 changed files with 127 additions and 37 deletions
|
@ -14,22 +14,37 @@ interface Props extends DivProps {
|
||||||
url?: string;
|
url?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
hasUpdates?: boolean;
|
||||||
preset?: typeof imagePresets[keyof typeof imagePresets];
|
preset?: typeof imagePresets[keyof typeof imagePresets];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar = forwardRef<HTMLDivElement, Props>(
|
const Avatar = forwardRef<HTMLDivElement, Props>(
|
||||||
(
|
(
|
||||||
{ url, username, size, className, preset = imagePresets.avatar, ...rest },
|
{
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
preset = imagePresets.avatar,
|
||||||
|
hasUpdates,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<Square
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
|
className={classNames(styles.container, {
|
||||||
|
[styles.has_dot]: hasUpdates,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Square
|
||||||
image={getURLFromString(url, preset) || '/images/john_doe.svg'}
|
image={getURLFromString(url, preset) || '/images/john_doe.svg'}
|
||||||
className={classNames(styles.avatar, className)}
|
className={classNames(styles.avatar, className)}
|
||||||
size={size}
|
size={size}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,20 @@
|
||||||
@import 'src/styles/variables';
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
&.has_dot::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $color_danger;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: $content_bg 0 0 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@include outer_shadow;
|
@include outer_shadow;
|
||||||
|
|
||||||
|
@ -12,6 +27,7 @@
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { FC } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import { imagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
@ -12,15 +11,21 @@ import styles from './styles.module.scss';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
username: string;
|
username: string;
|
||||||
photo?: IFile;
|
photo?: IFile;
|
||||||
|
hasUpdates?: boolean;
|
||||||
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserButton: FC<IProps> = ({ username, photo, onClick }) => {
|
const UserButton: FC<IProps> = ({ username, photo, hasUpdates, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<button className={styles.wrap} onClick={onClick}>
|
<button className={styles.wrap} onClick={onClick}>
|
||||||
<Group horizontal className={styles.user_button}>
|
<Group horizontal className={styles.user_button}>
|
||||||
<div className={styles.username}>{username}</div>
|
<div className={styles.username}>{username}</div>
|
||||||
<Avatar url={getURL(photo, imagePresets.avatar)} size={32} />
|
<Avatar
|
||||||
|
url={getURL(photo, imagePresets.avatar)}
|
||||||
|
size={32}
|
||||||
|
hasUpdates={hasUpdates}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Anchor } from '~/components/common/Anchor';
|
||||||
import { DivProps, LinkProps } from '~/utils/types';
|
import { DivProps, LinkProps } from '~/utils/types';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -11,7 +11,9 @@ interface VerticalMenuProps extends DivProps {
|
||||||
appearance?: 'inset' | 'flat' | 'default';
|
appearance?: 'inset' | 'flat' | 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VerticalMenuItemProps extends Omit<LinkProps, 'href'> {}
|
interface VerticalMenuItemProps extends Omit<LinkProps, 'href'> {
|
||||||
|
hasUpdates?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function VerticalMenu({
|
function VerticalMenu({
|
||||||
children,
|
children,
|
||||||
|
@ -28,8 +30,13 @@ function VerticalMenu({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalMenu.Item = ({ ...props }: VerticalMenuItemProps) => (
|
VerticalMenu.Item = ({ hasUpdates, ...props }: VerticalMenuItemProps) => (
|
||||||
<a {...props} className={classNames(styles.item, props.className)} />
|
<a
|
||||||
|
{...props}
|
||||||
|
className={classNames(styles.item, props.className, {
|
||||||
|
[styles.has_dot]: hasUpdates,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export { VerticalMenu };
|
export { VerticalMenu };
|
||||||
|
|
|
@ -33,6 +33,19 @@ a.item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
transition: background-color 0.25s;
|
transition: background-color 0.25s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.has_dot::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: $color_danger;
|
||||||
|
border-radius: 8px;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $content_bg_success;
|
background-color: $content_bg_success;
|
||||||
|
|
|
@ -9,16 +9,16 @@ import { Authorized } from '~/components/containers/Authorized';
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { Logo } from '~/components/main/Logo';
|
import { Logo } from '~/components/main/Logo';
|
||||||
import { UserButton } from '~/components/main/UserButton';
|
|
||||||
import { Dialog } from '~/constants/modal';
|
import { Dialog } from '~/constants/modal';
|
||||||
import { SidebarName } from '~/constants/sidebar';
|
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import { useScrollTop } from '~/hooks/dom/useScrollTop';
|
import { useScrollTop } from '~/hooks/dom/useScrollTop';
|
||||||
import { useFlow } from '~/hooks/flow/useFlow';
|
import { useFlow } from '~/hooks/flow/useFlow';
|
||||||
import { useModal } from '~/hooks/modal/useModal';
|
import { useModal } from '~/hooks/modal/useModal';
|
||||||
import { useUpdates } from '~/hooks/updates/useUpdates';
|
import { useUpdates } from '~/hooks/updates/useUpdates';
|
||||||
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
import { useNotifications } from '~/utils/providers/NotificationProvider';
|
||||||
|
|
||||||
|
import { UserButtonWithNotifications } from '../UserButtonWithNotifications';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
@ -30,11 +30,7 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
const { isUser, user, fetched } = useAuth();
|
const { isUser, user, fetched } = useAuth();
|
||||||
const { hasFlowUpdates, hasLabUpdates } = useFlow();
|
const { hasFlowUpdates, hasLabUpdates } = useFlow();
|
||||||
const { borisCommentedAt } = useUpdates();
|
const { borisCommentedAt } = useUpdates();
|
||||||
const { open } = useSidebar();
|
const { indicatorEnabled } = useNotifications();
|
||||||
|
|
||||||
const openProfileSidebar = useCallback(() => {
|
|
||||||
open(SidebarName.Settings, {});
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
|
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
|
||||||
|
|
||||||
|
@ -43,6 +39,7 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
const hasBorisUpdates = useMemo(
|
const hasBorisUpdates = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isUser &&
|
isUser &&
|
||||||
|
!indicatorEnabled &&
|
||||||
borisCommentedAt &&
|
borisCommentedAt &&
|
||||||
((fetched && !user.last_seen_boris) ||
|
((fetched && !user.last_seen_boris) ||
|
||||||
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
|
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
|
||||||
|
@ -67,14 +64,13 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
className={classNames(styles.plugs, {
|
className={classNames(styles.plugs, {
|
||||||
// [styles.active]: isHydrated && fetched,
|
|
||||||
[styles.active]: true,
|
[styles.active]: true,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Authorized hydratedOnly>
|
<Authorized hydratedOnly>
|
||||||
<Anchor
|
<Anchor
|
||||||
className={classNames(styles.item, {
|
className={classNames(styles.item, {
|
||||||
[styles.has_dot]: hasFlowUpdates,
|
[styles.has_dot]: hasFlowUpdates && !indicatorEnabled,
|
||||||
})}
|
})}
|
||||||
href={URLS.BASE}
|
href={URLS.BASE}
|
||||||
>
|
>
|
||||||
|
@ -83,7 +79,7 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
|
|
||||||
<Anchor
|
<Anchor
|
||||||
className={classNames(styles.item, styles.lab, {
|
className={classNames(styles.item, styles.lab, {
|
||||||
[styles.has_dot]: hasLabUpdates,
|
[styles.has_dot]: hasLabUpdates && !indicatorEnabled,
|
||||||
})}
|
})}
|
||||||
href={URLS.LAB}
|
href={URLS.LAB}
|
||||||
>
|
>
|
||||||
|
@ -92,7 +88,7 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
|
|
||||||
<Anchor
|
<Anchor
|
||||||
className={classNames(styles.item, styles.boris, {
|
className={classNames(styles.item, styles.boris, {
|
||||||
[styles.has_dot]: hasBorisUpdates,
|
[styles.has_dot]: hasBorisUpdates && !indicatorEnabled,
|
||||||
})}
|
})}
|
||||||
href={URLS.BORIS}
|
href={URLS.BORIS}
|
||||||
>
|
>
|
||||||
|
@ -101,13 +97,7 @@ const Header: FC<HeaderProps> = observer(() => {
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{isUser && (
|
{isUser && <UserButtonWithNotifications />}
|
||||||
<UserButton
|
|
||||||
username={user.username}
|
|
||||||
photo={user.photo}
|
|
||||||
onClick={openProfileSidebar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<Button className={styles.user_button} onClick={onLogin} round>
|
<Button className={styles.user_button} onClick={onLogin} round>
|
||||||
|
|
32
src/containers/main/UserButtonWithNotifications/index.tsx
Normal file
32
src/containers/main/UserButtonWithNotifications/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { UserButton } from '~/components/main/UserButton';
|
||||||
|
import { SidebarName } from '~/constants/sidebar';
|
||||||
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
|
import { useNotifications } from '~/utils/providers/NotificationProvider';
|
||||||
|
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
||||||
|
|
||||||
|
interface UserButtonWithNotificationsProps {}
|
||||||
|
|
||||||
|
const UserButtonWithNotifications: FC<
|
||||||
|
UserButtonWithNotificationsProps
|
||||||
|
> = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { open } = useSidebar();
|
||||||
|
const { hasNew, indicatorEnabled } = useNotifications();
|
||||||
|
|
||||||
|
const openProfileSidebar = useCallback(() => {
|
||||||
|
open(SidebarName.Settings, {});
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserButton
|
||||||
|
hasUpdates={hasNew && indicatorEnabled}
|
||||||
|
username={user.username}
|
||||||
|
photo={user.photo}
|
||||||
|
onClick={openProfileSidebar}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserButtonWithNotifications };
|
|
@ -13,6 +13,7 @@ import { ProfileStats } from '~/containers/profile/ProfileStats';
|
||||||
import { ThemeSwitcher } from '~/containers/settings/ThemeSwitcher';
|
import { ThemeSwitcher } from '~/containers/settings/ThemeSwitcher';
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import markdown from '~/styles/common/markdown.module.scss';
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
|
import { useNotifications } from '~/utils/providers/NotificationProvider';
|
||||||
|
|
||||||
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
||||||
import { ProfileToggles } from '../ProfileToggles';
|
import { ProfileToggles } from '../ProfileToggles';
|
||||||
|
@ -26,6 +27,7 @@ interface ProfileSidebarMenuProps {
|
||||||
const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const { setActiveTab } = useStackContext();
|
const { setActiveTab } = useStackContext();
|
||||||
|
const { hasNew } = useNotifications();
|
||||||
|
|
||||||
const onLogout = useCallback(() => {
|
const onLogout = useCallback(() => {
|
||||||
logout();
|
logout();
|
||||||
|
@ -46,7 +48,10 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
</VerticalMenu.Item>
|
</VerticalMenu.Item>
|
||||||
|
|
||||||
<Superpower>
|
<Superpower>
|
||||||
<VerticalMenu.Item onClick={() => setActiveTab(1)}>
|
<VerticalMenu.Item
|
||||||
|
onClick={() => setActiveTab(1)}
|
||||||
|
hasUpdates={hasNew}
|
||||||
|
>
|
||||||
Уведомления
|
Уведомления
|
||||||
</VerticalMenu.Item>
|
</VerticalMenu.Item>
|
||||||
</Superpower>
|
</Superpower>
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { useAuth } from '../auth/useAuth';
|
||||||
import { useNotificationSettingsRequest } from './useNotificationSettingsRequest';
|
import { useNotificationSettingsRequest } from './useNotificationSettingsRequest';
|
||||||
|
|
||||||
export const useNotificationSettings = () => {
|
export const useNotificationSettings = () => {
|
||||||
const { isUser } = useAuth();
|
// TODO: remove isTester
|
||||||
|
const { isUser, isTester } = useAuth();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error: settingsError,
|
error: settingsError,
|
||||||
|
@ -18,11 +19,15 @@ export const useNotificationSettings = () => {
|
||||||
update,
|
update,
|
||||||
} = useNotificationSettingsRequest();
|
} = useNotificationSettingsRequest();
|
||||||
|
|
||||||
const enabled = !isLoadingSettings && !settingsError && settingsEnabled;
|
const enabled =
|
||||||
|
!isLoadingSettings && !settingsError && settingsEnabled && isTester;
|
||||||
|
|
||||||
const hasNew =
|
const hasNew =
|
||||||
enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen));
|
enabled && !!lastDate && (!lastSeen || isAfter(lastDate, lastSeen));
|
||||||
|
|
||||||
|
// TODO: store `indicator` as option and include it here
|
||||||
|
const indicatorEnabled = enabled && true;
|
||||||
|
|
||||||
const markAsRead = useCallback(() => {
|
const markAsRead = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
!lastDate ||
|
!lastDate ||
|
||||||
|
@ -37,6 +42,7 @@ export const useNotificationSettings = () => {
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
hasNew,
|
hasNew,
|
||||||
|
indicatorEnabled,
|
||||||
available: isUser,
|
available: isUser,
|
||||||
markAsRead,
|
markAsRead,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ const defaultValue = {
|
||||||
available: false,
|
available: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
hasNew: false,
|
hasNew: false,
|
||||||
|
indicatorEnabled: false,
|
||||||
markAsRead: () => {},
|
markAsRead: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue