mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
Merge pull request #123 from muerwre/feature/122_new_profile_tooltip
#122 new profile tooltip
This commit is contained in:
commit
af5f060951
17 changed files with 281 additions and 201 deletions
|
@ -3,60 +3,26 @@ import React, { FC, useCallback, useState } from 'react';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { useRandomPhrase } from '~/constants/phrases';
|
import { MenuButton } from '~/components/menu';
|
||||||
|
import { ProfileQuickInfo } from '~/containers/profile/ProfileQuickInfo';
|
||||||
import { IUser } from '~/types/auth';
|
import { IUser } from '~/types/auth';
|
||||||
import { path } from '~/utils/ramda';
|
import { path } from '~/utils/ramda';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
withDetails: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = [
|
const CommentAvatar: FC<Props> = ({ user, className }) => {
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [0, 10],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const CommentAvatar: FC<Props> = ({ user, withDetails, className }) => {
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
|
|
||||||
|
|
||||||
const onMouseOver = useCallback(() => setHovered(true), [setHovered]);
|
|
||||||
const onMouseOut = useCallback(() => setHovered(false), [setHovered]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<MenuButton
|
||||||
<Reference>
|
position="auto"
|
||||||
{({ ref }) => (
|
icon={
|
||||||
<Avatar
|
<Avatar url={path(['photo', 'url'], user)} username={user.username} className={className} />
|
||||||
url={path(['photo', 'url'], user)}
|
}
|
||||||
username={user.username}
|
>
|
||||||
className={className}
|
<ProfileQuickInfo user={user} />
|
||||||
onMouseOver={onMouseOver}
|
</MenuButton>
|
||||||
onMouseOut={onMouseOut}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
|
|
||||||
{hovered && withDetails && (
|
|
||||||
<Popper placement="right" modifiers={modifiers} strategy="fixed">
|
|
||||||
{({ style, ref }) => (
|
|
||||||
<div style={style} ref={ref} className={styles.popper}>
|
|
||||||
<h4 className={styles.username}>{user.fullname || user.username}</h4>
|
|
||||||
<div className={styles.description}>{user.description || randomPhrase}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
)}
|
|
||||||
</Manager>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,11 @@ interface Props extends DivProps {
|
||||||
|
|
||||||
const Avatar = forwardRef<HTMLDivElement, Props>(
|
const Avatar = forwardRef<HTMLDivElement, Props>(
|
||||||
({ url, username, size, className, preset = ImagePresets.avatar, ...rest }, ref) => {
|
({ url, username, size, className, preset = ImagePresets.avatar, ...rest }, ref) => {
|
||||||
const onOpenProfile = useCallback(() => openUserProfile(username), [username]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Square
|
<Square
|
||||||
{...rest}
|
{...rest}
|
||||||
image={getURLFromString(url, preset)}
|
image={getURLFromString(url, preset)}
|
||||||
className={classNames(styles.avatar, className)}
|
className={classNames(styles.avatar, className)}
|
||||||
onClick={onOpenProfile}
|
|
||||||
size={size}
|
size={size}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -36,7 +36,7 @@ const CommentWrapper: FC<IProps> = ({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={styles.thumb}>
|
<div className={styles.thumb}>
|
||||||
<CommentAvatar user={user} className={styles.thumb_image} withDetails={!isForm} />
|
<CommentAvatar user={user} className={styles.thumb_image} />
|
||||||
<div className={styles.thumb_user}>~{path(['username'], user)}</div>
|
<div className={styles.thumb_user}>~{path(['username'], user)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import { MenuButton, MenuItemWithIcon } from '~/components/menu';
|
||||||
import { ImagePresets } from '~/constants/urls';
|
import { ImagePresets } from '~/constants/urls';
|
||||||
import { IUser } from '~/types/auth';
|
import { IUser } from '~/types/auth';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
|
@ -28,19 +29,23 @@ const UserButton: FC<IProps> = ({ user: { username, photo }, authOpenProfile, on
|
||||||
<Group horizontal className={styles.user_button}>
|
<Group horizontal className={styles.user_button}>
|
||||||
<div className={styles.username}>{username}</div>
|
<div className={styles.username}>{username}</div>
|
||||||
|
|
||||||
<div
|
<MenuButton
|
||||||
className={styles.user_avatar}
|
position="bottom"
|
||||||
style={{ backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')` }}
|
translucent={false}
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
className={styles.user_avatar}
|
||||||
|
style={{ backgroundImage: `url('${getURL(photo, ImagePresets.avatar)}')` }}
|
||||||
|
>
|
||||||
|
{(!photo || !photo.id) && <Icon icon="profile" />}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{(!photo || !photo.id) && <Icon icon="profile" />}
|
<MenuItemWithIcon onClick={onProfileOpen}>Профиль</MenuItemWithIcon>
|
||||||
</div>
|
<MenuItemWithIcon onClick={onSettingsOpen}>Настройки</MenuItemWithIcon>
|
||||||
|
<MenuItemWithIcon onClick={onLogout}>Выдох</MenuItemWithIcon>
|
||||||
|
</MenuButton>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.menu}>
|
|
||||||
<div onClick={onProfileOpen}>Профиль</div>
|
|
||||||
<div onClick={onSettingsOpen}>Настройки</div>
|
|
||||||
<div onClick={onLogout}>Выдох</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,63 +9,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
right: -$gap;
|
|
||||||
top: 100%;
|
|
||||||
padding: $gap;
|
|
||||||
border-radius: $radius;
|
|
||||||
display: none;
|
|
||||||
z-index: 1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: $gap;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: ' ';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 0 16px 16px;
|
|
||||||
border-color: transparent transparent $content_bg transparent;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: -4px;
|
|
||||||
transform: translate(-20px, 0);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
background: $content_bg;
|
|
||||||
padding: $gap $gap * 2;
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: opacity 0.25s;
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 40px;
|
|
||||||
transition: background-color 0.25s;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius: $radius;
|
|
||||||
border-top-right-radius: $radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom-left-radius: $radius;
|
|
||||||
border-bottom-right-radius: $radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > div {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user_button {
|
.user_button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
import { Placement } from '@popperjs/core';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { useFocusEvent } from '~/hooks/dom/useFocusEvent';
|
import { useFocusEvent } from '~/hooks/dom/useFocusEvent';
|
||||||
|
@ -9,55 +10,73 @@ import { useFocusEvent } from '~/hooks/dom/useFocusEvent';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface MenuButtonProps {
|
interface MenuButtonProps {
|
||||||
|
position?: Placement;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
activate?: 'hover' | 'focus';
|
||||||
|
fixed?: boolean;
|
||||||
|
translucent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = [
|
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [5, 10],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const MenuButton: FC<MenuButtonProps> = ({
|
const MenuButton: FC<MenuButtonProps> = ({
|
||||||
|
position = 'auto',
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
icon = <Icon icon="dots-vertical" size={24} />,
|
icon = <Icon icon="dots-vertical" size={24} />,
|
||||||
|
translucent = true,
|
||||||
|
activate = 'focus',
|
||||||
|
fixed,
|
||||||
}) => {
|
}) => {
|
||||||
const { focused, onFocus, onBlur } = useFocusEvent(false, 150);
|
const focus = useFocusEvent(false, 150);
|
||||||
|
const hover = useFocusEvent(false, 150);
|
||||||
|
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const popper = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: position,
|
||||||
|
strategy: fixed ? 'fixed' : 'absolute',
|
||||||
|
modifiers: [
|
||||||
|
{ name: 'arrow', options: { element: arrowElement } },
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [-10, 10],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = activate === 'focus' ? focus.focused : hover.focused;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<>
|
||||||
<Reference>
|
<button
|
||||||
{({ ref }) => (
|
className={classNames(styles.menu, className)}
|
||||||
<button
|
ref={setReferenceElement}
|
||||||
className={classNames(styles.menu, className)}
|
onFocus={focus.onFocus}
|
||||||
ref={ref}
|
onBlur={focus.onBlur}
|
||||||
onFocus={onFocus}
|
onMouseOver={hover.onFocus}
|
||||||
onBlur={onBlur}
|
onMouseOut={hover.onBlur}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
|
|
||||||
{focused && (
|
<div
|
||||||
<Popper placement="bottom-end" modifiers={modifiers}>
|
style={popper.styles.popper}
|
||||||
{({ style, ref, placement }) => (
|
ref={setPopperElement}
|
||||||
<div
|
{...popper.attributes.popper}
|
||||||
style={style}
|
className={classNames(styles.popper, {
|
||||||
ref={ref}
|
[styles.fixed]: fixed,
|
||||||
className={classNames(styles.popper, { [styles.top]: placement === 'top-end' })}
|
[styles.translucent]: translucent,
|
||||||
>
|
[styles.visible]: visible,
|
||||||
{children}
|
})}
|
||||||
</div>
|
>
|
||||||
)}
|
<div style={popper.styles.arrow} ref={setArrowElement} className={styles.arrow} />
|
||||||
</Popper>
|
{children}
|
||||||
)}
|
</div>
|
||||||
</Manager>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,36 +5,59 @@
|
||||||
|
|
||||||
@import "src/styles/variables.scss";
|
@import "src/styles/variables.scss";
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
0% { opacity: 0 }
|
|
||||||
100% { opacity: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
.popper {
|
.popper {
|
||||||
@include outer_shadow;
|
@include outer_shadow;
|
||||||
|
|
||||||
background-color: $menu_bg;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
animation: appear forwards 250ms;
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
touch-action: none;
|
||||||
|
background-color: transparentize($content_bg, 0.05);
|
||||||
|
|
||||||
&::after {
|
&.visible {
|
||||||
content: ' ';
|
visibility: visible;
|
||||||
width: 0;
|
pointer-events: all;
|
||||||
height: 0;
|
touch-action: initial;
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 10px 10px 10px;
|
|
||||||
border-color: transparent transparent lighten($menu_bg, 6%) transparent;
|
|
||||||
position: absolute;
|
|
||||||
top: -11px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.top::after {
|
&.fixed {
|
||||||
border-width: 10px 10px 0 10px;
|
z-index: 100;
|
||||||
border-color: darken($menu_bg, 8%) transparent transparent transparent;
|
}
|
||||||
top: auto;
|
|
||||||
bottom: -11px;
|
&.translucent {
|
||||||
|
@include blur($content_bg, 15px, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
[data-popper-placement*='bottom'] & {
|
||||||
|
border-width: 0 10px 10px 10px;
|
||||||
|
border-color: transparent transparent lighten($menu_bg, 6%) transparent;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-popper-placement*='top'] & {
|
||||||
|
border-width: 10px 10px 0 10px;
|
||||||
|
border-color: lighten($menu_bg, 6%) transparent transparent transparent;
|
||||||
|
bottom: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-popper-placement*='right'] & {
|
||||||
|
border-width: 10px 10px 10px 0;
|
||||||
|
border-color: transparent lighten($menu_bg, 6%) transparent transparent;
|
||||||
|
left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-popper-placement*='left'] & {
|
||||||
|
border-width: 10px 0 10px 10px;
|
||||||
|
border-color: transparent transparent transparent lighten($menu_bg, 6%);
|
||||||
|
right: -10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,17 @@ import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface MenuItemWithIconProps {
|
interface MenuItemWithIconProps {
|
||||||
children: string;
|
children: string;
|
||||||
icon: string;
|
icon?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItemWithIcon: FC<MenuItemWithIconProps> = ({ children, icon, onClick }) => (
|
const MenuItemWithIcon: FC<MenuItemWithIconProps> = ({ children, icon, onClick }) => (
|
||||||
<button className={styles.item} onClick={onClick}>
|
<button className={styles.item} onClick={onClick}>
|
||||||
<div className={styles.icon}>
|
{icon && (
|
||||||
<Icon icon={icon} size={20} />
|
<div className={styles.icon}>
|
||||||
</div>
|
<Icon icon={icon} size={20} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.text}>{children}</div>
|
<div className={styles.text}>{children}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -18,12 +18,11 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
margin-right: $gap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-right: $gap;
|
padding: 0 $gap;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
|
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
|
||||||
transform: translate(0, -$header_height);
|
transform: translate(0, -$header_height);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
src/containers/profile/ProfileQuickInfo/index.tsx
Normal file
45
src/containers/profile/ProfileQuickInfo/index.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { useRandomPhrase } from '~/constants/phrases';
|
||||||
|
import { useUserActiveStatus } from '~/hooks/auth/useUserActiveStatus';
|
||||||
|
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
||||||
|
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||||
|
import { IUser } from '~/types/auth';
|
||||||
|
import { generateGradientFromColor } from '~/utils/color';
|
||||||
|
import { path } from '~/utils/ramda';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface ProfileQuickInfoProps {
|
||||||
|
user: IUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileQuickInfo: FC<ProfileQuickInfoProps> = ({ user }) => {
|
||||||
|
const isActive = useUserActiveStatus(user.last_seen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group className={styles.wrapper}>
|
||||||
|
<Group className={styles.top} horizontal>
|
||||||
|
<div>
|
||||||
|
<Avatar url={path(['photo', 'url'], user)} username={user.username} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Filler className={styles.names}>
|
||||||
|
<h5 className={styles.fullname}>{user.fullname || user.username}</h5>
|
||||||
|
<div className={styles.username}>~{user.username}</div>
|
||||||
|
|
||||||
|
<div className={classNames(styles.status, { [styles.active]: isActive })}>
|
||||||
|
{isActive ? 'в сознании' : 'деактивирован'}
|
||||||
|
</div>
|
||||||
|
</Filler>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ProfileQuickInfo };
|
47
src/containers/profile/ProfileQuickInfo/styles.module.scss
Normal file
47
src/containers/profile/ProfileQuickInfo/styles.module.scss
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
@include outer_shadow;
|
||||||
|
|
||||||
|
padding: $gap $gap * 2 $gap $gap;
|
||||||
|
border-radius: $radius;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.info {
|
||||||
|
font: $font_12_regular;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.top.top {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.names {
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font: $font_12_regular;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullname {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font: $font_12_regular;
|
||||||
|
margin-top: $gap;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $olive;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '●';
|
||||||
|
margin-right: 5px;
|
||||||
|
padding-top: 1px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
14
src/hooks/auth/useUserActiveStatus.ts
Normal file
14
src/hooks/auth/useUserActiveStatus.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { differenceInDays, parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
export const useUserActiveStatus = (lastSeen?: string) => {
|
||||||
|
try {
|
||||||
|
const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined;
|
||||||
|
return lastSeenDate && differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,11 +1,7 @@
|
||||||
import { differenceInDays, parseISO } from 'date-fns';
|
|
||||||
|
|
||||||
import { useRandomPhrase } from '~/constants/phrases';
|
import { useRandomPhrase } from '~/constants/phrases';
|
||||||
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';
|
import { useUserActiveStatus } from '~/hooks/auth/useUserActiveStatus';
|
||||||
import { IUser } from '~/types/auth';
|
import { IUser } from '~/types/auth';
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
export const useUserDescription = (user?: Partial<IUser>) => {
|
export const useUserDescription = (user?: Partial<IUser>) => {
|
||||||
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
|
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
|
||||||
|
|
||||||
|
@ -13,8 +9,9 @@ export const useUserDescription = (user?: Partial<IUser>) => {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSeen = user.last_seen ? parseISO(user.last_seen) : undefined;
|
const isActive = useUserActiveStatus(user.last_seen);
|
||||||
if (!lastSeen || differenceInDays(today, lastSeen) > INACTIVE_ACCOUNT_DAYS) {
|
|
||||||
|
if (!isActive) {
|
||||||
return 'Юнит деактивирован';
|
return 'Юнит деактивирован';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import { adjustHue } from 'color2k';
|
import { adjustHue } from 'color2k';
|
||||||
|
|
||||||
import { normalizeBrightColor } from '~/utils/color';
|
import { generateGradientFromColor, normalizeBrightColor } from '~/utils/color';
|
||||||
import { stringToColour } from '~/utils/dom';
|
import { stringToColour } from '~/utils/dom';
|
||||||
|
|
||||||
export const useColorGradientFromString = (
|
export const useColorGradientFromString = (
|
||||||
|
@ -16,9 +16,5 @@ export const useColorGradientFromString = (
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = normalizeBrightColor(stringToColour(val), saturation, lightness);
|
return generateGradientFromColor(val, saturation, lightness, angle);
|
||||||
const second = normalizeBrightColor(adjustHue(color, 45), saturation, lightness);
|
|
||||||
const third = normalizeBrightColor(adjustHue(color, 90), saturation, lightness);
|
|
||||||
|
|
||||||
return `linear-gradient(${angle}deg, ${color}, ${second}, ${third})`;
|
|
||||||
}, [angle, lightness, saturation, val]);
|
}, [angle, lightness, saturation, val]);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { darken, desaturate, parseToHsla } from 'color2k';
|
import { adjustHue, darken, desaturate, parseToHsla, transparentize } from 'color2k';
|
||||||
|
|
||||||
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
|
||||||
|
import { stringToColour } from '~/utils/dom';
|
||||||
|
|
||||||
export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnessExp = 3) => {
|
export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnessExp = 3) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
|
@ -14,3 +15,31 @@ export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnes
|
||||||
const desaturated = saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color;
|
const desaturated = saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color;
|
||||||
return lightnessExp > 1 ? darken(desaturated, lightness ** lightnessExp) : desaturated;
|
return lightnessExp > 1 ? darken(desaturated, lightness ** lightnessExp) : desaturated;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateColorTriplet = (val: string, saturation: number, lightness: number) => {
|
||||||
|
const color = normalizeBrightColor(stringToColour(val), saturation, lightness);
|
||||||
|
|
||||||
|
return [
|
||||||
|
color,
|
||||||
|
normalizeBrightColor(adjustHue(color, 45), saturation, lightness),
|
||||||
|
normalizeBrightColor(adjustHue(color, 90), saturation, lightness),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateGradientFromColor = (
|
||||||
|
val: string,
|
||||||
|
saturation = 3,
|
||||||
|
lightness = 3,
|
||||||
|
angle = 155,
|
||||||
|
opacity = 1
|
||||||
|
) => {
|
||||||
|
const [first, second, third] = generateColorTriplet(val, saturation, lightness).map(it => {
|
||||||
|
if (opacity > 1 || opacity < 0) {
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transparentize(it, 1 - opacity);
|
||||||
|
});
|
||||||
|
|
||||||
|
return `linear-gradient(${angle}deg, ${first}, ${second}, ${third})`;
|
||||||
|
};
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue