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

notifications: added profile indicator

This commit is contained in:
Fedor Katurov 2023-03-12 11:07:32 +06:00
parent 97590d88af
commit dc90f2505c
10 changed files with 127 additions and 37 deletions

View file

@ -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}
image={getURLFromString(url, preset) || '/images/john_doe.svg'} className={classNames(styles.container, {
className={classNames(styles.avatar, className)} [styles.has_dot]: hasUpdates,
size={size} })}
ref={ref} >
/> <Square
image={getURLFromString(url, preset) || '/images/john_doe.svg'}
className={classNames(styles.avatar, className)}
size={size}
ref={ref}
/>
</div>
); );
}, },
); );

View file

@ -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;

View file

@ -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>
); );

View file

@ -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 };

View file

@ -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;

View file

@ -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>

View 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 };

View file

@ -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>

View file

@ -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,
}; };

View file

@ -10,6 +10,7 @@ const defaultValue = {
available: false, available: false,
enabled: false, enabled: false,
hasNew: false, hasNew: false,
indicatorEnabled: false,
markAsRead: () => {}, markAsRead: () => {},
}; };