mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
notifications: for nodes
This commit is contained in:
parent
14bf5be65f
commit
d9544e917b
13 changed files with 156 additions and 27 deletions
|
@ -1,10 +1,9 @@
|
||||||
import React, { FC, MouseEventHandler } from 'react';
|
import { FC, MouseEventHandler } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Anchor } from '~/components/common/Anchor';
|
import { Anchor } from '~/components/common/Anchor';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { NodeThumbnail } from '~/components/node/NodeThumbnail';
|
||||||
import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
|
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { INode } from '~/types';
|
import { INode } from '~/types';
|
||||||
import { getPrettyDate } from '~/utils/dom';
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
@ -31,7 +30,7 @@ const FlowRecentItem: FC<IProps> = ({ node, has_new, onClick }) => {
|
||||||
[styles.lab]: !node.is_promoted,
|
[styles.lab]: !node.is_promoted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<NodeRelatedItem item={node} />
|
<NodeThumbnail item={node} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { FC, ReactElement } from 'react';
|
||||||
import { Hoverable } from '~/components/common/Hoverable';
|
import { Hoverable } from '~/components/common/Hoverable';
|
||||||
import { SubTitle } from '~/components/common/SubTitle';
|
import { SubTitle } from '~/components/common/SubTitle';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
|
import { NodeThumbnail } from '~/components/node/NodeThumbnail';
|
||||||
import { INode } from '~/types';
|
import { INode } from '~/types';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -21,7 +21,7 @@ const NodeRelated: FC<IProps> = ({ title, items }) => {
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Hoverable key={item.id} className={styles.item}>
|
<Hoverable key={item.id} className={styles.item}>
|
||||||
<NodeRelatedItem item={item} />
|
<NodeThumbnail item={item} />
|
||||||
</Hoverable>
|
</Hoverable>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { FC, memo } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import cell_style from '~/components/node/NodeRelatedItem/styles.module.scss';
|
import cell_style from '~/components/node/NodeThumbnail/styles.module.scss';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
import { range } from '~/utils/ramda';
|
import { range } from '~/utils/ramda';
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,17 @@ import { Icon } from '~/components/input/Icon';
|
||||||
import { imagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
|
||||||
import { useGotoNode } from '~/hooks/node/useGotoNode';
|
import { useGotoNode } from '~/hooks/node/useGotoNode';
|
||||||
import { INode } from '~/types';
|
|
||||||
import { getURL, getURLFromString } from '~/utils/dom';
|
import { getURL, getURLFromString } from '~/utils/dom';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
type IProps = {
|
type NodeThumbnailProps = {
|
||||||
item: Partial<INode>;
|
item: {
|
||||||
|
thumbnail?: string;
|
||||||
|
title?: string;
|
||||||
|
is_promoted?: boolean;
|
||||||
|
id?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type CellSize = 'small' | 'medium' | 'large';
|
type CellSize = 'small' | 'medium' | 'large';
|
||||||
|
@ -33,7 +37,7 @@ const getTitleLetters = (title?: string): string => {
|
||||||
: words[0].substr(0, 2).toUpperCase();
|
: words[0].substr(0, 2).toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
const NodeThumbnail: FC<NodeThumbnailProps> = memo(({ item }) => {
|
||||||
const onClick = useGotoNode(item.id);
|
const onClick = useGotoNode(item.id);
|
||||||
const [is_loaded, setIsLoaded] = useState(false);
|
const [is_loaded, setIsLoaded] = useState(false);
|
||||||
const [width, setWidth] = useState(0);
|
const [width, setWidth] = useState(0);
|
||||||
|
@ -118,4 +122,4 @@ const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export { NodeRelatedItem };
|
export { NodeThumbnail };
|
|
@ -1,6 +1,7 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Anchor } from '~/components/common/Anchor';
|
import { Anchor } from '~/components/common/Anchor';
|
||||||
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
import { InlineUsername } from '~/components/common/InlineUsername';
|
import { InlineUsername } from '~/components/common/InlineUsername';
|
||||||
import { Square } from '~/components/common/Square';
|
import { Square } from '~/components/common/Square';
|
||||||
import { NotificationItem } from '~/types/notifications';
|
import { NotificationItem } from '~/types/notifications';
|
||||||
|
@ -16,8 +17,10 @@ const NotificationComment: FC<NotificationCommentProps> = ({ item }) => (
|
||||||
<Anchor href={item.url} className={styles.link}>
|
<Anchor href={item.url} className={styles.link}>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
<Square
|
<Avatar
|
||||||
image={getURLFromString(item.user.photo, 'avatar')}
|
size={32}
|
||||||
|
url={item.user?.photo}
|
||||||
|
username={item.user?.username}
|
||||||
className={styles.circle}
|
className={styles.circle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,9 +28,17 @@ const NotificationComment: FC<NotificationCommentProps> = ({ item }) => (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<b className={styles.title}>
|
<b className={styles.title}>
|
||||||
<span>
|
<span>
|
||||||
<InlineUsername>{item.user.username}</InlineUsername>:
|
<InlineUsername>{item.user.username}</InlineUsername>
|
||||||
</span>
|
</span>
|
||||||
|
<span>-</span>
|
||||||
|
<Square
|
||||||
|
className={styles.item_image}
|
||||||
|
size={16}
|
||||||
|
image={getURLFromString(item.thumbnail, 'avatar')}
|
||||||
|
/>
|
||||||
|
<div className={styles.item_title}>{item.title}</div>
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
.message {
|
.message {
|
||||||
font: $font_14_regular;
|
font: $font_14_regular;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
padding: $gap/2 $gap/2 $gap/4 $gap/2;
|
|
||||||
min-height: calc(1.3em * 3 + $gap);
|
min-height: calc(1.3em * 3 + $gap);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px auto;
|
grid-template-columns: 32px auto;
|
||||||
|
@ -18,15 +17,16 @@
|
||||||
.content {
|
.content {
|
||||||
background: $content_bg;
|
background: $content_bg;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 0 4px 4px 4px;
|
border-radius: 0 $radius $radius $radius;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $gap;
|
top: 8px;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
@include arrow_left(10px, $content_bg);
|
@include arrow_left(8px, $content_bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +36,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font: $font_14_semibold;
|
font: $font_14_medium;
|
||||||
margin-bottom: $gap / 2;
|
margin-bottom: $gap / 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_title {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 5px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_image {
|
||||||
|
flex: 0 0 16px;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
|
@ -47,6 +67,6 @@
|
||||||
color: $gray_75;
|
color: $gray_75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle {
|
div.circle {
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
39
src/components/notifications/NotificationNode/index.tsx
Normal file
39
src/components/notifications/NotificationNode/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { FC, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { NodeThumbnail } from '~/components/node/NodeThumbnail';
|
||||||
|
import { NotificationItem } from '~/types/notifications';
|
||||||
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface NotificationNodeProps {
|
||||||
|
item: NotificationItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationNode: FC<NotificationNodeProps> = ({ item }) => {
|
||||||
|
const thumbnail = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: item.title,
|
||||||
|
thumbnail: item.thumbnail,
|
||||||
|
is_promoted: true,
|
||||||
|
}),
|
||||||
|
[item],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.image}>
|
||||||
|
<NodeThumbnail item={thumbnail} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.text}>
|
||||||
|
<div className={styles.title}>{item.title || '...'}</div>
|
||||||
|
<div className={styles.user}>
|
||||||
|
~{item.user.username}, {getPrettyDate(item.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationNode };
|
|
@ -0,0 +1,36 @@
|
||||||
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: $content_bg;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
border-radius: $radius;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: $gap/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: $gap;
|
||||||
|
margin-top: -0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: $gray_50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { FC, useEffect } from 'react';
|
import { FC, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -6,7 +6,9 @@ import { Button } from '~/components/input/Button';
|
||||||
import { InputRow } from '~/components/input/InputRow';
|
import { InputRow } from '~/components/input/InputRow';
|
||||||
import { LoaderScreen } from '~/components/input/LoaderScreen';
|
import { LoaderScreen } from '~/components/input/LoaderScreen';
|
||||||
import { NotificationComment } from '~/components/notifications/NotificationComment';
|
import { NotificationComment } from '~/components/notifications/NotificationComment';
|
||||||
|
import { NotificationNode } from '~/components/notifications/NotificationNode';
|
||||||
import { useNotificationsList } from '~/hooks/notifications/useNotificationsList';
|
import { useNotificationsList } from '~/hooks/notifications/useNotificationsList';
|
||||||
|
import { NotificationItem, NotificationType } from '~/types/notifications';
|
||||||
import { useNotifications } from '~/utils/providers/NotificationProvider';
|
import { useNotifications } from '~/utils/providers/NotificationProvider';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -22,6 +24,17 @@ const NotificationList: FC<NotificationListProps> = () => {
|
||||||
return () => markAsRead();
|
return () => markAsRead();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderItem = useCallback((item: NotificationItem) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case NotificationType.Comment:
|
||||||
|
return <NotificationComment item={item} />;
|
||||||
|
case NotificationType.Node:
|
||||||
|
return <NotificationNode item={item} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoaderScreen align="top" />;
|
return <LoaderScreen align="top" />;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +59,7 @@ const NotificationList: FC<NotificationListProps> = () => {
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
{items?.map((item) => (
|
{items?.map((item) => (
|
||||||
<div className={styles.item} key={item.created_at}>
|
<div className={styles.item} key={item.created_at}>
|
||||||
<NotificationComment item={item} />
|
{renderItem(item)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,8 +47,9 @@
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@include row_shadow;
|
@include row_shadow;
|
||||||
padding-right: $gap;
|
padding: $gap / 2 $gap $gap / 2 $gap / 2;
|
||||||
transition: background-color 0.25s;
|
transition: background-color 0.25s;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $content_bg_lighter;
|
background-color: $content_bg_lighter;
|
||||||
|
|
|
@ -2,10 +2,15 @@ import { useCallback } from 'react';
|
||||||
|
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { useNavigation } from '~/hooks/navigation/useNavigation';
|
import { useNavigation } from '~/hooks/navigation/useNavigation';
|
||||||
import { INode } from '~/types';
|
|
||||||
|
|
||||||
// useGotoNode returns fn, that navigates to node
|
// useGotoNode returns fn, that navigates to node
|
||||||
export const useGotoNode = (id: INode['id']) => {
|
export const useGotoNode = (id?: number) => {
|
||||||
const { push } = useNavigation();
|
const { push } = useNavigation();
|
||||||
return useCallback(() => push(URLS.NODE_URL(id)), [push, id]);
|
return useCallback(() => {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(URLS.NODE_URL(id));
|
||||||
|
}, [push, id]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ export interface NotificationItem {
|
||||||
id: number;
|
id: number;
|
||||||
url: string;
|
url: string;
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
user: ShallowUser;
|
user: ShallowUser;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue