mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
add backlinks
This commit is contained in:
parent
6222b75563
commit
811e7740a9
21 changed files with 257 additions and 56 deletions
|
@ -34,6 +34,11 @@ module.exports = withBundleAnalyzer(
|
||||||
hostname: '*.ytimg.com',
|
hostname: '*.ytimg.com',
|
||||||
pathname: '/**',
|
pathname: '/**',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'localhost',
|
||||||
|
pathname: '/**',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -76,7 +76,11 @@ export const apiGetNode = (
|
||||||
api
|
api
|
||||||
.get<ApiGetNodeResponse>(API.NODES.GET(id), config)
|
.get<ApiGetNodeResponse>(API.NODES.GET(id), config)
|
||||||
.then(cleanResult)
|
.then(cleanResult)
|
||||||
.then((data) => ({ node: data.node, last_seen: data.last_seen }));
|
.then((data) => ({
|
||||||
|
node: data.node,
|
||||||
|
last_seen: data.last_seen,
|
||||||
|
backlinks: data.backlinks,
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
||||||
const cancelToken = axios.CancelToken.source();
|
const cancelToken = axios.CancelToken.source();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC, ReactNode, useCallback } from 'react';
|
import React, { FC, ReactNode, useCallback } from 'react';
|
||||||
|
|
||||||
import { Group } from '~/components/containers/Group';
|
import { WithDescription } from '~/components/common/WithDescription';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -31,20 +31,12 @@ const BorisContactItem: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{prefix}
|
{prefix}
|
||||||
<div
|
<WithDescription
|
||||||
onClick={onClick}
|
icon={<Icon icon={icon} size={32} />}
|
||||||
className={styles.item}
|
title={title}
|
||||||
role={link ? 'button' : 'none'}
|
link={link}
|
||||||
>
|
subtitle={subtitle}
|
||||||
<div className={styles.icon}>
|
/>
|
||||||
<Icon icon={icon} size={32} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.title}>{title}</div>
|
|
||||||
<div className={styles.subtitle}>{subtitle}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{suffix}
|
{suffix}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
39
src/components/common/WithDescription/index.tsx
Normal file
39
src/components/common/WithDescription/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { FC, ReactNode, useCallback } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WithDescription: FC<Props> = ({ icon, title, subtitle, link }) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
window.open(link);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(styles.item, { [styles.link]: link })}
|
||||||
|
role={link ? 'button' : 'none'}
|
||||||
|
>
|
||||||
|
<div className={styles.icon}>{icon}</div>
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
{!!subtitle?.trim() && (
|
||||||
|
<div className={styles.subtitle}>{subtitle}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WithDescription };
|
|
@ -9,11 +9,14 @@
|
||||||
color: $gray_50;
|
color: $gray_50;
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
|
|
||||||
|
&.link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
height: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
|
@ -8,10 +8,25 @@ import styles from './styles.module.scss';
|
||||||
|
|
||||||
export type CardProps = DivProps & {
|
export type CardProps = DivProps & {
|
||||||
seamless?: boolean;
|
seamless?: boolean;
|
||||||
|
elevation?: -1 | 0 | 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Card: FC<CardProps> = ({ className, children, seamless, ...props }) => (
|
const Card: FC<CardProps> = ({
|
||||||
<div className={classNames(styles.card, className, { seamless })} {...props}>
|
className,
|
||||||
|
children,
|
||||||
|
seamless,
|
||||||
|
elevation = 1,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.card,
|
||||||
|
{ seamless },
|
||||||
|
styles[`elevation-${elevation}`],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,18 @@
|
||||||
border-radius: $panel_radius;
|
border-radius: $panel_radius;
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
|
|
||||||
|
&.elevation--1 {
|
||||||
|
@include inner_shadow;
|
||||||
|
background: linear-gradient(135deg, $content_bg_dark, $content_bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.elevation-1 {
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
|
}
|
||||||
|
|
||||||
|
&.elevation-0 {
|
||||||
|
background: $content_bg_light;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.seamless) {
|
&:global(.seamless) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
22
src/components/node/Backlink/index.tsx
Normal file
22
src/components/node/Backlink/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { WithDescription } from '~/components/common/WithDescription';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
|
||||||
|
interface BacklinkProps {
|
||||||
|
icon?: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Backlink: FC<BacklinkProps> = ({ icon, title, subtitle, link }) => (
|
||||||
|
<WithDescription
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
icon={icon && <Icon icon={icon} />}
|
||||||
|
link={link}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Backlink };
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '~/components/common/Avatar';
|
import { Avatar } from '~/components/common/Avatar';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
import { useUserDescription } from '~/hooks/auth/useUserDescription';
|
||||||
import { INodeUser } from '~/types';
|
import { INodeUser } from '~/types';
|
||||||
|
|
||||||
|
@ -20,14 +21,14 @@ const NodeAuthorBlock: FC<Props> = ({ user }) => {
|
||||||
const { fullname, username, photo } = user;
|
const { fullname, username, photo } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.block}>
|
<Card className={styles.block} elevation={-1}>
|
||||||
<Avatar username={username} url={photo?.url} className={styles.avatar} />
|
<Avatar username={username} url={photo?.url} className={styles.avatar} />
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.username}>{fullname || username}</div>
|
<div className={styles.username}>{fullname || username}</div>
|
||||||
<div className={styles.description}>{description}</div>
|
<div className={styles.description}>{description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
@import 'src/styles/variables.scss';
|
@import 'src/styles/variables.scss';
|
||||||
|
|
||||||
div.block {
|
div.block {
|
||||||
@include inner_shadow_active;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
background: linear-gradient(135deg, $content_bg_dark, $content_bg);
|
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -13,9 +9,6 @@ div.block {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
font: $font_16_semibold;
|
font: $font_16_semibold;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
|
|
|
@ -5,3 +5,9 @@ export const SOCIAL_ICONS: Record<OAuthProvider, string> = {
|
||||||
google: 'google',
|
google: 'google',
|
||||||
telegram: 'telegram',
|
telegram: 'telegram',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BacklinkSource = 'vkontakte';
|
||||||
|
|
||||||
|
export const BACKLINK_TITLES: Record<BacklinkSource, string> = {
|
||||||
|
vkontakte: 'Суицидальные роботы',
|
||||||
|
};
|
||||||
|
|
52
src/containers/node/NodeBacklinks/index.tsx
Normal file
52
src/containers/node/NodeBacklinks/index.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { FC, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { SubTitle } from '~/components/common/SubTitle';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Padder } from '~/components/containers/Padder';
|
||||||
|
import { Backlink } from '~/components/node/Backlink';
|
||||||
|
import { NodeBackLink } from '~/types';
|
||||||
|
import { has } from '~/utils/ramda';
|
||||||
|
|
||||||
|
import { BACKLINK_TITLES, SOCIAL_ICONS } from '../../../constants/auth/socials';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface NodeBacklinksProps {
|
||||||
|
list?: NodeBackLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeBacklinks: FC<NodeBacklinksProps> = ({ list }) => {
|
||||||
|
const validBacklinks = useMemo(
|
||||||
|
() => (list || []).filter((it) => it.provider && it.link),
|
||||||
|
[list],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validBacklinks.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SubTitle className={styles.subtitle}>Расшарено:</SubTitle>
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{validBacklinks.map((it) => (
|
||||||
|
<Card elevation={-1} seamless key={it.link} className={styles.card}>
|
||||||
|
<Backlink
|
||||||
|
icon={SOCIAL_ICONS[it.provider]}
|
||||||
|
title={
|
||||||
|
has(it.provider, BACKLINK_TITLES)
|
||||||
|
? BACKLINK_TITLES[it.provider]
|
||||||
|
: it.provider
|
||||||
|
}
|
||||||
|
subtitle={it.provider}
|
||||||
|
link={it.link}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeBacklinks };
|
14
src/containers/node/NodeBacklinks/styles.module.scss
Normal file
14
src/containers/node/NodeBacklinks/styles.module.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@import '~/styles/variables.scss';
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.card {
|
||||||
|
padding-right: $gap !important;
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Padder } from '~/components/containers/Padder';
|
import { Padder } from '~/components/containers/Padder';
|
||||||
import { Sticky } from '~/components/containers/Sticky';
|
import { Sticky } from '~/components/containers/Sticky';
|
||||||
|
@ -9,6 +11,7 @@ import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
||||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
|
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
|
||||||
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
|
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
|
||||||
|
import { NodeBacklinks } from '~/containers/node/NodeBacklinks';
|
||||||
import { NodeComments } from '~/containers/node/NodeComments';
|
import { NodeComments } from '~/containers/node/NodeComments';
|
||||||
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
|
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
|
||||||
import { useCommentContext } from '~/utils/context/CommentContextProvider';
|
import { useCommentContext } from '~/utils/context/CommentContextProvider';
|
||||||
|
@ -25,7 +28,7 @@ interface IProps {
|
||||||
|
|
||||||
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
const { node, isLoading } = useNodeContext();
|
const { node, isLoading, backlinks } = useNodeContext();
|
||||||
const {
|
const {
|
||||||
comments,
|
comments,
|
||||||
isLoading: isLoadingComments,
|
isLoading: isLoadingComments,
|
||||||
|
@ -63,6 +66,12 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className={styles.subheader}>
|
||||||
|
<Filler className={styles.backlinks}>
|
||||||
|
<NodeBacklinks list={backlinks} />
|
||||||
|
</Filler>
|
||||||
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<aside className={styles.panel}>
|
<aside className={styles.panel}>
|
||||||
|
|
|
@ -46,3 +46,15 @@
|
||||||
.left_item {
|
.left_item {
|
||||||
padding-bottom: $gap * 2;
|
padding-bottom: $gap * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subheader {
|
||||||
|
display: flex;
|
||||||
|
gap: $gap;
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlinks {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const useLoadNode = (id: number, fallbackData?: ApiGetNodeResponse) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: data?.node || EMPTY_NODE,
|
node: data?.node || EMPTY_NODE,
|
||||||
|
backlinks: data?.backlinks,
|
||||||
isLoading: isValidating && !data,
|
isLoading: isValidating && !data,
|
||||||
update,
|
update,
|
||||||
lastSeen: data?.last_seen,
|
lastSeen: data?.last_seen,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Footer } from '~/components/main/Footer';
|
||||||
import { NodeTitle } from '~/components/node/NodeTitle';
|
import { NodeTitle } from '~/components/node/NodeTitle';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
|
import { NodeBacklinks } from '~/containers/node/NodeBacklinks';
|
||||||
import { NodeBottomBlock } from '~/containers/node/NodeBottomBlock';
|
import { NodeBottomBlock } from '~/containers/node/NodeBottomBlock';
|
||||||
import { useNodeActions } from '~/hooks/node/useNodeActions';
|
import { useNodeActions } from '~/hooks/node/useNodeActions';
|
||||||
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
|
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
|
||||||
|
|
|
@ -103,7 +103,7 @@ type Props = RouteComponentProps<{ id: string }> &
|
||||||
|
|
||||||
const NodePage: FC<Props> = observer((props) => {
|
const NodePage: FC<Props> = observer((props) => {
|
||||||
const id = useNodePageParams();
|
const id = useNodePageParams();
|
||||||
const { node, isLoading, update, lastSeen } = useLoadNode(
|
const { node, isLoading, update, lastSeen, backlinks } = useLoadNode(
|
||||||
parseInt(id, 10),
|
parseInt(id, 10),
|
||||||
props.fallbackData,
|
props.fallbackData,
|
||||||
);
|
);
|
||||||
|
@ -133,7 +133,12 @@ const NodePage: FC<Props> = observer((props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
<NodeContextProvider
|
||||||
|
node={node}
|
||||||
|
isLoading={isLoading}
|
||||||
|
update={update}
|
||||||
|
backlinks={backlinks}
|
||||||
|
>
|
||||||
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
||||||
<CommentContextProvider
|
<CommentContextProvider
|
||||||
onSaveComment={onSaveComment}
|
onSaveComment={onSaveComment}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Context } from "react";
|
import { Context } from 'react';
|
||||||
|
|
||||||
import { ERRORS } from "~/constants/errors";
|
import { ERRORS } from '~/constants/errors';
|
||||||
import { IUser } from "~/types/auth";
|
import { IUser } from '~/types/auth';
|
||||||
|
|
||||||
export interface ITag {
|
export interface ITag {
|
||||||
ID: number;
|
ID: number;
|
||||||
|
@ -22,7 +22,7 @@ export type ContextValue<T> = T extends Context<infer U> ? U : never;
|
||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
export type IUploadType = "image" | "text" | "audio" | "video" | "other";
|
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -55,21 +55,21 @@ export interface IFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockText {
|
export interface IBlockText {
|
||||||
type: "text";
|
type: 'text';
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockEmbed {
|
export interface IBlockEmbed {
|
||||||
type: "video";
|
type: 'video';
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IBlock = IBlockText | IBlockEmbed;
|
export type IBlock = IBlockText | IBlockEmbed;
|
||||||
export type FlowDisplayVariant =
|
export type FlowDisplayVariant =
|
||||||
| "single"
|
| 'single'
|
||||||
| "vertical"
|
| 'vertical'
|
||||||
| "horizontal"
|
| 'horizontal'
|
||||||
| "quadro";
|
| 'quadro';
|
||||||
export interface FlowDisplay {
|
export interface FlowDisplay {
|
||||||
display: FlowDisplayVariant;
|
display: FlowDisplayVariant;
|
||||||
show_description: boolean;
|
show_description: boolean;
|
||||||
|
@ -98,6 +98,7 @@ export interface INode {
|
||||||
like_count?: number;
|
like_count?: number;
|
||||||
|
|
||||||
flow: FlowDisplay;
|
flow: FlowDisplay;
|
||||||
|
backlinks?: NodeBackLink[];
|
||||||
|
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
|
|
||||||
|
@ -109,9 +110,13 @@ export interface INode {
|
||||||
|
|
||||||
export type IFlowNode = Pick<
|
export type IFlowNode = Pick<
|
||||||
INode,
|
INode,
|
||||||
"id" | "flow" | "description" | "title" | "thumbnail" | "created_at"
|
'id' | 'flow' | 'description' | 'title' | 'thumbnail' | 'created_at'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export interface NodeBackLink {
|
||||||
|
provider: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
export interface IComment {
|
export interface IComment {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -123,7 +128,7 @@ export interface IComment {
|
||||||
deleted_at?: string;
|
deleted_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IMessage = Omit<IComment, "user" | "node"> & {
|
export type IMessage = Omit<IComment, 'user' | 'node'> & {
|
||||||
from: IUser;
|
from: IUser;
|
||||||
to: IUser;
|
to: IUser;
|
||||||
};
|
};
|
||||||
|
@ -132,7 +137,7 @@ export interface ICommentGroup {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
distancesInDays: number[];
|
distancesInDays: number[];
|
||||||
ids: IComment["id"][];
|
ids: IComment['id'][];
|
||||||
hasNew: boolean;
|
hasNew: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,19 +145,19 @@ export type IUploadProgressHandler = (progress: ProgressEvent) => void;
|
||||||
export type IError = ValueOf<typeof ERRORS>;
|
export type IError = ValueOf<typeof ERRORS>;
|
||||||
|
|
||||||
export const NOTIFICATION_TYPES = {
|
export const NOTIFICATION_TYPES = {
|
||||||
message: "message",
|
message: 'message',
|
||||||
comment: "comment",
|
comment: 'comment',
|
||||||
node: "node",
|
node: 'node',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IMessageNotification = {
|
export type IMessageNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES["message"];
|
type: typeof NOTIFICATION_TYPES['message'];
|
||||||
content: Partial<IMessage>;
|
content: Partial<IMessage>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ICommentNotification = {
|
export type ICommentNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES["comment"];
|
type: typeof NOTIFICATION_TYPES['comment'];
|
||||||
content: Partial<IComment>;
|
content: Partial<IComment>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IComment, INode, ITag } from '~/types';
|
import { IComment, INode, ITag, NodeBackLink } from '~/types';
|
||||||
|
|
||||||
export interface IEditorComponentProps {}
|
export interface IEditorComponentProps {}
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ export type PostCellViewResult = unknown; // TODO: update it with actual type
|
||||||
export type ApiGetNodeRequest = {
|
export type ApiGetNodeRequest = {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
};
|
};
|
||||||
export type ApiGetNodeResponse = { node: INode; last_seen?: string | null };
|
export type ApiGetNodeResponse = {
|
||||||
|
node: INode;
|
||||||
|
backlinks?: NodeBackLink[];
|
||||||
|
last_seen?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiGetNodeRelatedRequest = {
|
export type ApiGetNodeRelatedRequest = {
|
||||||
id: INode['id'];
|
id: INode['id'];
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
import React, { createContext, FC, useContext } from 'react';
|
import React, { createContext, FC, useContext } from 'react';
|
||||||
|
|
||||||
import { EMPTY_NODE } from '~/constants/node';
|
import { EMPTY_NODE } from '~/constants/node';
|
||||||
import { INode } from '~/types';
|
import { INode, NodeBackLink } from '~/types';
|
||||||
|
|
||||||
export interface NodeContextProps {
|
export interface NodeContextProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
backlinks?: NodeBackLink[];
|
||||||
update: (node: Partial<INode>) => Promise<unknown>;
|
update: (node: Partial<INode>) => Promise<unknown>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeContext = createContext<NodeContextProps>({
|
export const NodeContext = createContext<NodeContextProps>({
|
||||||
node: EMPTY_NODE,
|
node: EMPTY_NODE,
|
||||||
|
backlinks: [] as NodeBackLink[] | undefined,
|
||||||
update: async () => {},
|
update: async () => {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeContextProvider: FC<NodeContextProps> = ({ children, ...contextValue }) => {
|
export const NodeContextProvider: FC<NodeContextProps> = ({
|
||||||
return <NodeContext.Provider value={contextValue}>{children}</NodeContext.Provider>;
|
children,
|
||||||
|
...contextValue
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<NodeContext.Provider value={contextValue}>{children}</NodeContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useNodeContext = () => useContext(NodeContext);
|
export const useNodeContext = () => useContext(NodeContext);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue