diff --git a/next.config.js b/next.config.js index b3b05794..8fb923d7 100644 --- a/next.config.js +++ b/next.config.js @@ -34,6 +34,11 @@ module.exports = withBundleAnalyzer( hostname: '*.ytimg.com', pathname: '/**', }, + { + protocol: 'http', + hostname: 'localhost', + pathname: '/**', + }, ], }, }) diff --git a/src/api/node/index.ts b/src/api/node/index.ts index 820143eb..9ae675cc 100644 --- a/src/api/node/index.ts +++ b/src/api/node/index.ts @@ -76,7 +76,11 @@ export const apiGetNode = ( api .get(API.NODES.GET(id), config) .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) => { const cancelToken = axios.CancelToken.source(); diff --git a/src/components/boris/BorisContactItem/index.tsx b/src/components/boris/BorisContactItem/index.tsx index e3d06310..bc2178b5 100644 --- a/src/components/boris/BorisContactItem/index.tsx +++ b/src/components/boris/BorisContactItem/index.tsx @@ -1,6 +1,6 @@ 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 styles from './styles.module.scss'; @@ -31,20 +31,12 @@ const BorisContactItem: FC = ({ return (
{prefix} -
-
- -
- -
-
{title}
-
{subtitle}
-
-
+ } + title={title} + link={link} + subtitle={subtitle} + /> {suffix}
); diff --git a/src/components/common/WithDescription/index.tsx b/src/components/common/WithDescription/index.tsx new file mode 100644 index 00000000..0a311046 --- /dev/null +++ b/src/components/common/WithDescription/index.tsx @@ -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 = ({ icon, title, subtitle, link }) => { + const onClick = useCallback(() => { + if (!link) return; + + window.open(link); + }, []); + + return ( +
+
{icon}
+ +
+
{title}
+ {!!subtitle?.trim() && ( +
{subtitle}
+ )} +
+
+ ); +}; + +export { WithDescription }; diff --git a/src/components/boris/BorisContactItem/styles.module.scss b/src/components/common/WithDescription/styles.module.scss similarity index 91% rename from src/components/boris/BorisContactItem/styles.module.scss rename to src/components/common/WithDescription/styles.module.scss index 7c0aec80..d1917902 100644 --- a/src/components/boris/BorisContactItem/styles.module.scss +++ b/src/components/common/WithDescription/styles.module.scss @@ -9,11 +9,14 @@ color: $gray_50; padding: $gap; min-height: 42px; + + &.link { + cursor: pointer; + } } .icon { fill: currentColor; - height: 32px; } .info { diff --git a/src/components/containers/Card/index.tsx b/src/components/containers/Card/index.tsx index 58e8e80f..70576544 100644 --- a/src/components/containers/Card/index.tsx +++ b/src/components/containers/Card/index.tsx @@ -8,10 +8,25 @@ import styles from './styles.module.scss'; export type CardProps = DivProps & { seamless?: boolean; + elevation?: -1 | 0 | 1; }; -const Card: FC = ({ className, children, seamless, ...props }) => ( -
+const Card: FC = ({ + className, + children, + seamless, + elevation = 1, + ...props +}) => ( +
{children}
); diff --git a/src/components/containers/Card/styles.module.scss b/src/components/containers/Card/styles.module.scss index f2ac642e..e330c9b5 100644 --- a/src/components/containers/Card/styles.module.scss +++ b/src/components/containers/Card/styles.module.scss @@ -5,7 +5,18 @@ border-radius: $panel_radius; padding: $gap; - @include outer_shadow(); + &.elevation--1 { + @include inner_shadow; + background: linear-gradient(135deg, $content_bg_dark, $content_bg); + } + + &.elevation-1 { + @include outer_shadow(); + } + + &.elevation-0 { + background: $content_bg_light; + } &:global(.seamless) { padding: 0; diff --git a/src/components/node/Backlink/index.tsx b/src/components/node/Backlink/index.tsx new file mode 100644 index 00000000..d3cbb32b --- /dev/null +++ b/src/components/node/Backlink/index.tsx @@ -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 = ({ icon, title, subtitle, link }) => ( + } + link={link} + /> +); + +export { Backlink }; diff --git a/src/components/node/NodeAuthorBlock/index.tsx b/src/components/node/NodeAuthorBlock/index.tsx index 94f35103..b1f55586 100644 --- a/src/components/node/NodeAuthorBlock/index.tsx +++ b/src/components/node/NodeAuthorBlock/index.tsx @@ -1,6 +1,7 @@ -import React, { FC, useCallback } from 'react'; +import React, { FC } from 'react'; import { Avatar } from '~/components/common/Avatar'; +import { Card } from '~/components/containers/Card'; import { useUserDescription } from '~/hooks/auth/useUserDescription'; import { INodeUser } from '~/types'; @@ -20,14 +21,14 @@ const NodeAuthorBlock: FC = ({ user }) => { const { fullname, username, photo } = user; return ( -
+
{fullname || username}
{description}
-
+ ); }; diff --git a/src/components/node/NodeAuthorBlock/styles.module.scss b/src/components/node/NodeAuthorBlock/styles.module.scss index 8f7e9124..3f01e1fc 100644 --- a/src/components/node/NodeAuthorBlock/styles.module.scss +++ b/src/components/node/NodeAuthorBlock/styles.module.scss @@ -1,10 +1,6 @@ @import 'src/styles/variables.scss'; div.block { - @include inner_shadow_active; - - cursor: pointer; - background: linear-gradient(135deg, $content_bg_dark, $content_bg); padding: $gap; border-radius: $radius; display: flex; @@ -13,9 +9,6 @@ div.block { justify-content: stretch; } -.info { -} - .username { font: $font_16_semibold; line-height: 21px; diff --git a/src/constants/auth/socials.ts b/src/constants/auth/socials.ts index f9d24fd6..1c7a71f7 100644 --- a/src/constants/auth/socials.ts +++ b/src/constants/auth/socials.ts @@ -5,3 +5,9 @@ export const SOCIAL_ICONS: Record = { google: 'google', telegram: 'telegram', }; + +export type BacklinkSource = 'vkontakte'; + +export const BACKLINK_TITLES: Record = { + vkontakte: 'Суицидальные роботы', +}; diff --git a/src/containers/node/NodeBacklinks/index.tsx b/src/containers/node/NodeBacklinks/index.tsx new file mode 100644 index 00000000..cf8515ad --- /dev/null +++ b/src/containers/node/NodeBacklinks/index.tsx @@ -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 = ({ list }) => { + const validBacklinks = useMemo( + () => (list || []).filter((it) => it.provider && it.link), + [list], + ); + + if (!validBacklinks.length) { + return null; + } + + return ( +
+ Расшарено: + +
+ {validBacklinks.map((it) => ( + + + + ))} +
+
+ ); +}; + +export { NodeBacklinks }; diff --git a/src/containers/node/NodeBacklinks/styles.module.scss b/src/containers/node/NodeBacklinks/styles.module.scss new file mode 100644 index 00000000..48022c67 --- /dev/null +++ b/src/containers/node/NodeBacklinks/styles.module.scss @@ -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; +} diff --git a/src/containers/node/NodeBottomBlock/index.tsx b/src/containers/node/NodeBottomBlock/index.tsx index 6553b7f7..22c8ba50 100644 --- a/src/containers/node/NodeBottomBlock/index.tsx +++ b/src/containers/node/NodeBottomBlock/index.tsx @@ -1,5 +1,7 @@ import React, { FC } from 'react'; +import { Card } from '~/components/containers/Card'; +import { Filler } from '~/components/containers/Filler'; import { Group } from '~/components/containers/Group'; import { Padder } from '~/components/containers/Padder'; import { Sticky } from '~/components/containers/Sticky'; @@ -9,6 +11,7 @@ import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock'; import { NodeTagsBlock } from '~/components/node/NodeTagsBlock'; +import { NodeBacklinks } from '~/containers/node/NodeBacklinks'; import { NodeComments } from '~/containers/node/NodeComments'; import { useNodeBlocks } from '~/hooks/node/useNodeBlocks'; import { useCommentContext } from '~/utils/context/CommentContextProvider'; @@ -25,7 +28,7 @@ interface IProps { const NodeBottomBlock: FC = ({ commentsOrder }) => { const user = useUserContext(); - const { node, isLoading } = useNodeContext(); + const { node, isLoading, backlinks } = useNodeContext(); const { comments, isLoading: isLoadingComments, @@ -63,6 +66,12 @@ const NodeBottomBlock: FC = ({ commentsOrder }) => { user={user} /> )} + +
+ + + +