diff --git a/.drone.yml b/.drone.yml index 282a923e..0ddbd29c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -78,7 +78,7 @@ steps: format: markdown message: > {{#success build.status}}πŸ€“{{else}}😨{{/success}} - {{ datetime build.finished "01.02.2006 15:04:05" "UTC" }} [{{repo.name}} / {{commit.branch}}]({{ build.link }}) + [{{repo.name}} / {{commit.branch}}]({{ build.link }}) ``` {{ commit.message }} ``` diff --git a/src/components/comment/CommentTextBlock/styles.module.scss b/src/components/comment/CommentTextBlock/styles.module.scss index ec47d8fd..09814017 100644 --- a/src/components/comment/CommentTextBlock/styles.module.scss +++ b/src/components/comment/CommentTextBlock/styles.module.scss @@ -28,4 +28,12 @@ :global(.green) { color: $wisegreen; } + + & > :last-child::after { + display: inline-block; + content: " "; + height: 1em; + width: 120px; + flex: 0 0 120px; + } } diff --git a/src/components/containers/Authorized/index.tsx b/src/components/containers/Authorized/index.tsx new file mode 100644 index 00000000..e0b6085b --- /dev/null +++ b/src/components/containers/Authorized/index.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { selectUser } from '~/redux/auth/selectors'; + +interface IProps {} + +const Authorized: FC = ({ children }) => { + const user = useShallowSelect(selectUser); + + if (!user.is_user) return null; + + return <>{children}; +}; + +export { Authorized }; diff --git a/src/components/containers/Markdown/index.tsx b/src/components/containers/Markdown/index.tsx new file mode 100644 index 00000000..3ff0397f --- /dev/null +++ b/src/components/containers/Markdown/index.tsx @@ -0,0 +1,11 @@ +import React, { ButtonHTMLAttributes, DetailedHTMLProps, FC, HTMLAttributes } from 'react'; +import styles from '~/styles/common/markdown.module.scss'; +import classNames from 'classnames'; + +interface IProps extends DetailedHTMLProps, HTMLDivElement> {} + +const Markdown: FC = ({ className, ...props }) => ( +
+); + +export { Markdown }; diff --git a/src/components/editors/EditorPublicSwitch/index.tsx b/src/components/editors/EditorPublicSwitch/index.tsx index 71c1fef2..627316fb 100644 --- a/src/components/editors/EditorPublicSwitch/index.tsx +++ b/src/components/editors/EditorPublicSwitch/index.tsx @@ -14,29 +14,27 @@ const EditorPublicSwitch: FC = ({ data, setData }) => { ]); return ( - - - + ); }; diff --git a/src/components/lab/LabBanner/index.tsx b/src/components/lab/LabBanner/index.tsx index df8f60ea..856a7844 100644 --- a/src/components/lab/LabBanner/index.tsx +++ b/src/components/lab/LabBanner/index.tsx @@ -9,12 +9,21 @@ interface IProps {} const LabBanner: FC = () => ( - - - - - - +
Лаборатория!
+ + +

+ + Всё, Ρ‡Ρ‚ΠΎ происходит здСсь — всСго лишь экспСримСнт, ΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π½Π΅ ΡƒΠ·Π½Π°Π΅Ρ‚ Π½ΠΈΠΊΡ‚ΠΎ Π·Π° + ΠΏΡ€Π΅Π΄Π΅Π»Π°ΠΌΠΈ Π£Π±Π΅ΠΆΠΈΡ‰Π°. + +

+ +

+ Π›ΠΎΠ²ΠΈΠΌ Ρ€Π°Π΄ΠΈΠΎΠ°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… ΠΆΡƒΠΊΠΎΠ², ΠΏΡ€ΠΈΡ€ΡƒΡ‡Π°Π΅ΠΌ утконосов-Π²Π°ΠΌΠΏΠΈΡ€ΠΎΠ², катаСмся Π½Π° младшСм Π½Π°ΡƒΡ‡Π½ΠΎΠΌ + сотрудникС Π•Π³ΠΎΡ€Π΅ ΠŸΠΎΡ€ΡΠΈΡ„ΠΎΡ€ΠΎΠ²ΠΈΡ‡Π΅ (Ρƒ Π½Π΅Π³ΠΎ ΠΊΠ°ΠΊ Ρ€Π°Π· сСйчас линька). +

+
); diff --git a/src/components/lab/LabBanner/styles.module.scss b/src/components/lab/LabBanner/styles.module.scss index f235ed41..4f4f6174 100644 --- a/src/components/lab/LabBanner/styles.module.scss +++ b/src/components/lab/LabBanner/styles.module.scss @@ -1,5 +1,21 @@ @import "~/styles/variables.scss"; .wrap { - background: $red_gradient_alt; + @include lab_shadow; + + background: linear-gradient(darken($dark_blue, 0%), darken($blue, 30%)); +} + +.title { + font: $font_24_bold; + text-transform: uppercase; +} + +.content { + font: $font_14_regular; + line-height: 19px; + + strong { + font-weight: bold; + } } diff --git a/src/components/lab/LabBottomPanel/index.tsx b/src/components/lab/LabBottomPanel/index.tsx new file mode 100644 index 00000000..5139686a --- /dev/null +++ b/src/components/lab/LabBottomPanel/index.tsx @@ -0,0 +1,49 @@ +import React, { FC, useCallback } from 'react'; +import { Group } from '~/components/containers/Group'; +import { Filler } from '~/components/containers/Filler'; +import styles from './styles.module.scss'; +import { getPrettyDate } from '~/utils/dom'; +import { INode } from '~/redux/types'; +import { Icon } from '~/components/input/Icon'; +import classNames from 'classnames'; +import { Grid } from '~/components/containers/Grid'; +import { useHistory } from 'react-router'; +import { URLS } from '~/constants/urls'; + +type Props = { + node: INode; + isLoading?: boolean; + hasNewComments: boolean; + commentCount: number; +}; + +const LabBottomPanel: FC = ({ node, hasNewComments, commentCount }) => { + const history = useHistory(); + const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]); + + return ( + +
{getPrettyDate(node.created_at)}
+ + + {commentCount > 0 && ( + + + {commentCount} + + )} + + {!!node.like_count && node.like_count > 0 && ( + + + {node.like_count} + + )} +
+ ); +}; + +export { LabBottomPanel }; diff --git a/src/components/lab/LabBottomPanel/styles.module.scss b/src/components/lab/LabBottomPanel/styles.module.scss new file mode 100644 index 00000000..09a69c46 --- /dev/null +++ b/src/components/lab/LabBottomPanel/styles.module.scss @@ -0,0 +1,25 @@ +@import "~/styles/variables.scss"; + +.wrap { + padding: 0 $gap $gap; +} + +.timestamp { + font: $font_12_regular; + color: darken(white, 40%); +} + +.comments, .like { + flex: 0; + font: $font_14_semibold; + color: darken(white, 50%); + fill: currentColor; + stroke: none; + column-gap: $gap / 2 !important; + align-items: center; + justify-content: center; + + &.active { + color: $red; + } +} diff --git a/src/components/lab/LabHead/index.tsx b/src/components/lab/LabHead/index.tsx index 25fb6cfd..cce1ff2a 100644 --- a/src/components/lab/LabHead/index.tsx +++ b/src/components/lab/LabHead/index.tsx @@ -1,32 +1,31 @@ import React, { FC } from 'react'; -import { Group } from '~/components/containers/Group'; -import { Card } from '~/components/containers/Card'; -import { Placeholder } from '~/components/placeholders/Placeholder'; -import { Filler } from '~/components/containers/Filler'; +import styles from './styles.module.scss'; +import { LabHeadItem } from '~/components/lab/LabHeadItem'; -interface IProps {} +interface IProps { + isLoading?: boolean; +} -const LabHead: FC = () => ( - - - - - - +const LabHead: FC = ({ isLoading }) => { + return null; - - - - + return ( +
+
+ + Π‘Π²Π΅ΠΆΠΈΠ΅ + - - - - + + ΠŸΠΎΠΏΡƒΠ»ΡΡ€Π½Ρ‹Π΅ + - - - -); + + Π’Π°ΠΆΠ½Ρ‹Π΅ + +
+
+ ); +}; export { LabHead }; diff --git a/src/components/lab/LabHead/styles.module.scss b/src/components/lab/LabHead/styles.module.scss new file mode 100644 index 00000000..0848772f --- /dev/null +++ b/src/components/lab/LabHead/styles.module.scss @@ -0,0 +1,23 @@ +@import "~/styles/variables.scss"; + +.wrap { + @include lab_shadow; + + border-radius: $radius; + background-color: $content_bg; + padding: $gap / 2; +} + +.group { + display: flex; + + @include tablet { + flex-wrap: wrap; + align-items: center; + justify-content: center; + } + + & > * { + margin: $gap / 2; + } +} diff --git a/src/components/lab/LabHeadItem/index.tsx b/src/components/lab/LabHeadItem/index.tsx new file mode 100644 index 00000000..b4f51583 --- /dev/null +++ b/src/components/lab/LabHeadItem/index.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import { Group } from '~/components/containers/Group'; +import { Icon } from '~/components/input/Icon'; +import { Placeholder } from '~/components/placeholders/Placeholder'; +import styles from './styles.module.scss'; +import classNames from 'classnames'; + +interface IProps { + icon: string; + isLoading?: boolean; + active?: boolean; +} + +const LabHeadItem: FC = ({ icon, children, isLoading, active }) => { + if (isLoading) { + return ( + + + + + ); + } + + return ( + + + {children} + + ); +}; + +export { LabHeadItem }; diff --git a/src/components/lab/LabHeadItem/styles.module.scss b/src/components/lab/LabHeadItem/styles.module.scss new file mode 100644 index 00000000..cb5b360e --- /dev/null +++ b/src/components/lab/LabHeadItem/styles.module.scss @@ -0,0 +1,25 @@ +@import "~/styles/variables.scss"; + +.item { + flex: 0 0 auto; + padding: $gap / 2; + fill: currentColor; + color: darken(white, 50%); + transition: color 0.25s; + cursor: pointer; + + &:hover { + color: white; + } + + &.active { + color: $blue; + background-color: lighten($content_bg, 6%); + border-radius: $radius; + padding: 0 $gap; + } +} + +.text { + font: $font_16_semibold; +} diff --git a/src/components/lab/LabHero/index.tsx b/src/components/lab/LabHero/index.tsx index 1a059815..be4d28d1 100644 --- a/src/components/lab/LabHero/index.tsx +++ b/src/components/lab/LabHero/index.tsx @@ -1,22 +1,51 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { Placeholder } from '~/components/placeholders/Placeholder'; import { Group } from '~/components/containers/Group'; import { Icon } from '~/components/input/Icon'; import styles from './styles.module.scss'; +import { INode } from '~/redux/types'; +import { getPrettyDate } from '~/utils/dom'; +import { URLS } from '~/constants/urls'; +import { Link, useHistory } from 'react-router-dom'; -interface IProps {} +interface IProps { + node?: Partial; + isLoading?: boolean; +} -const LabHero: FC = () => ( - -
- -
+const LabHero: FC = ({ node, isLoading }) => { + const history = useHistory(); + const onClick = useCallback(() => { + history.push(URLS.NODE_URL(node?.id)); + }, [history, node]); - - - + if (!node || isLoading) { + return ( + +
+ +
+ +
+ + +
+
+ ); + } + + return ( + +
+ +
+ +
+
{node.title}
+
{getPrettyDate(node.created_at)}
+
-
-); + ); +}; export { LabHero }; diff --git a/src/components/lab/LabHero/styles.module.scss b/src/components/lab/LabHero/styles.module.scss index b1a7e9cc..bb6f9b8d 100644 --- a/src/components/lab/LabHero/styles.module.scss +++ b/src/components/lab/LabHero/styles.module.scss @@ -1,10 +1,34 @@ @import "~/styles/variables.scss"; .wrap { - margin-bottom: $gap; + min-width: 0; + text-decoration: none; + cursor: pointer; } .star { - fill: #2c2c2c; + fill: darken(white, 76%); + flex: 0 0 32px; +} + +.title { + font: $font_18_semibold; + text-overflow: ellipsis; + line-height: 22px; + word-break: break-all; + color: darken(white, 40%); + + @include clamp(2, 22px) +} + +.description { + font: $font_10_regular; + color: darken(white, 50%); + padding-top: 4px; +} + +.content { + padding: $gap / 2 0; + text-decoration: none; } diff --git a/src/components/lab/LabHeroes/index.tsx b/src/components/lab/LabHeroes/index.tsx new file mode 100644 index 00000000..9c820682 --- /dev/null +++ b/src/components/lab/LabHeroes/index.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; +import { INode } from '~/redux/types'; +import styles from '~/containers/lab/LabStats/styles.module.scss'; +import { LabHero } from '~/components/lab/LabHero'; +import { Group } from '~/components/containers/Group'; + +interface IProps { + nodes: Partial[]; + isLoading: boolean; +} + +const empty = [...new Array(5)].map((_, i) => i); + +const LabHeroes: FC = ({ nodes, isLoading }) => { + if (isLoading) { + return ( + + {empty.map(i => ( + + ))} + + ); + } + + return ( + + {nodes.slice(0, 10).map(node => ( + + ))} + + ); +}; + +export { LabHeroes }; diff --git a/src/components/lab/LabImage/index.tsx b/src/components/lab/LabImage/index.tsx new file mode 100644 index 00000000..18d4e5d5 --- /dev/null +++ b/src/components/lab/LabImage/index.tsx @@ -0,0 +1,100 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { INodeComponentProps } from '~/redux/node/constants'; +import SwiperCore, { A11y, Pagination, Navigation, SwiperOptions, Keyboard } from 'swiper'; +import { Swiper, SwiperSlide } from 'swiper/react'; + +import 'swiper/swiper.scss'; +import 'swiper/components/pagination/pagination.scss'; +import 'swiper/components/scrollbar/scrollbar.scss'; +import 'swiper/components/zoom/zoom.scss'; +import 'swiper/components/navigation/navigation.scss'; + +import styles from './styles.module.scss'; +import { useNodeImages } from '~/utils/hooks/node/useNodeImages'; +import { getURL } from '~/utils/dom'; +import { PRESETS, URLS } from '~/constants/urls'; +import SwiperClass from 'swiper/types/swiper-class'; +import { modalShowPhotoswipe } from '~/redux/modal/actions'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; + +SwiperCore.use([Navigation, Pagination, A11y]); + +interface IProps extends INodeComponentProps {} + +const breakpoints: SwiperOptions['breakpoints'] = { + 599: { + spaceBetween: 20, + navigation: true, + }, +}; + +const LabImage: FC = ({ node }) => { + const history = useHistory(); + + const [controlledSwiper, setControlledSwiper] = useState(undefined); + + const images = useNodeImages(node); + + const updateSwiper = useCallback(() => { + if (!controlledSwiper) return; + + controlledSwiper.updateSlides(); + controlledSwiper.updateSize(); + controlledSwiper.update(); + }, [controlledSwiper]); + + const resetSwiper = useCallback(() => { + if (!controlledSwiper) return; + controlledSwiper.slideTo(0, 0); + }, [controlledSwiper]); + + useEffect(() => { + updateSwiper(); + resetSwiper(); + }, [images, updateSwiper, resetSwiper]); + + const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [history, node.id]); + + if (!images?.length) { + return null; + } + + return ( +
+ 1 ? 1.1 : 1} + onSwiper={setControlledSwiper} + spaceBetween={10} + grabCursor + autoHeight + breakpoints={breakpoints} + observeSlideChildren + observeParents + resizeObserver + watchOverflow + updateOnImagesReady + onInit={resetSwiper} + keyboard={{ + enabled: true, + onlyInViewport: false, + }} + > + {images.map(file => ( + + {node.title} + + ))} + +
+ ); +}; + +export { LabImage }; diff --git a/src/components/lab/LabImage/styles.module.scss b/src/components/lab/LabImage/styles.module.scss new file mode 100644 index 00000000..308253be --- /dev/null +++ b/src/components/lab/LabImage/styles.module.scss @@ -0,0 +1,69 @@ +@import "~/styles/variables.scss"; + +.wrapper { + border-radius: $radius; + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + + :global(.swiper-container) { + width: 100%; + } + + :global(.swiper-button-next), + :global(.swiper-button-prev) { + color: white; + font-size: 10px; + + &::after { + font-size: 32px; + } + } + +} + +.slide { + text-align: center; + text-transform: uppercase; + font: $font_32_bold; + display: flex; + border-radius: $radius; + align-items: center; + justify-content: center; + width: auto; + max-width: 100%; + opacity: 1; + filter: brightness(50%) saturate(0.5); + transition: opacity 0.5s, filter 0.5s, transform 0.5s; + + &:global(.swiper-slide-active) { + opacity: 1; + filter: brightness(100%); + } + + @include tablet { + padding-bottom: 0; + padding-top: 0; + } +} + +.image { + max-height: calc(100vh - 70px - 70px); + max-width: 100%; + border-radius: $radius; + transition: box-shadow 1s; + box-shadow: transparentize(black, 0.7) 0 3px 5px; + + :global(.swiper-slide-active) & { + box-shadow: transparentize(black, 0.9) 0 10px 5px 4px, + transparentize(black, 0.7) 0 5px 5px, + transparentize(white, 0.95) 0 -1px 2px, + transparentize(white, 0.95) 0 -1px; + } + + @include tablet { + padding-bottom: 0; + max-height: 100vh; + } +} diff --git a/src/components/lab/LabNode/index.tsx b/src/components/lab/LabNode/index.tsx index 5f64c55f..94dc699d 100644 --- a/src/components/lab/LabNode/index.tsx +++ b/src/components/lab/LabNode/index.tsx @@ -1,30 +1,36 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { INode } from '~/redux/types'; -import { NodePanelInner } from '~/components/node/NodePanelInner'; import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks'; import styles from './styles.module.scss'; -import { Card } from '~/components/containers/Card'; -import { NodePanelLab } from '~/components/node/NodePanelLab'; +import { LabBottomPanel } from '~/components/lab/LabBottomPanel'; +import { isAfter, parseISO } from 'date-fns'; interface IProps { node: INode; + lastSeen: string | null; + isLoading?: boolean; + commentCount: number; } -const LabNode: FC = ({ node }) => { - const { inline, block, head } = useNodeBlocks(node, false); +const LabNode: FC = ({ node, isLoading, lastSeen, commentCount }) => { + const { lab } = useNodeBlocks(node, false); - console.log(node.id, { inline, block, head }); + const hasNewComments = useMemo( + () => + !!node.commented_at && !!lastSeen && isAfter(parseISO(node.commented_at), parseISO(lastSeen)), + [node.commented_at, lastSeen] + ); return ( - -
- -
- - {head} - {block} - {inline} -
+
+ {lab} + +
); }; diff --git a/src/components/lab/LabNode/styles.module.scss b/src/components/lab/LabNode/styles.module.scss index 0e8e59ab..14052071 100644 --- a/src/components/lab/LabNode/styles.module.scss +++ b/src/components/lab/LabNode/styles.module.scss @@ -1,11 +1,12 @@ @import "~/styles/variables.scss"; .wrap { + @include lab_shadow; + + background-color: $lab_post_bg; + cursor: pointer; + min-width: 0; -} - -.head { - background-color: transparentize(black, 0.9); - border-radius: $radius $radius 0 0; + border-radius: $radius; } diff --git a/src/components/lab/LabNodeTitle/index.tsx b/src/components/lab/LabNodeTitle/index.tsx new file mode 100644 index 00000000..b44533de --- /dev/null +++ b/src/components/lab/LabNodeTitle/index.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; +import { INode } from '~/redux/types'; +import styles from './styles.module.scss'; +import { URLS } from '~/constants/urls'; +import { Link } from 'react-router-dom'; +import { Group } from '~/components/containers/Group'; +import { Icon } from '~/components/input/Icon'; +import Tippy from '@tippy.js/react'; + +interface IProps { + node: INode; +} + +const LabNodeTitle: FC = ({ node }) => { + if (!node.title) return null; + + return ( + +
+ {node.title || '...'} +
+ + {node.is_heroic && ( + +
+ +
+
+ )} +
+ ); +}; + +export { LabNodeTitle }; diff --git a/src/components/node/NodePanelLab/styles.module.scss b/src/components/lab/LabNodeTitle/styles.module.scss similarity index 76% rename from src/components/node/NodePanelLab/styles.module.scss rename to src/components/lab/LabNodeTitle/styles.module.scss index 095dafe5..f9ee925c 100644 --- a/src/components/node/NodePanelLab/styles.module.scss +++ b/src/components/lab/LabNodeTitle/styles.module.scss @@ -1,7 +1,7 @@ @import "~/styles/variables.scss"; .wrap { - padding: $gap; + padding: 0 $gap; } .title { @@ -19,6 +19,11 @@ @include tablet { white-space: nowrap; padding-bottom: 0; - font: $font_16_semibold; + font: $font_20_semibold; } } + +.star { + fill: $yellow; + flex: 0 0 24px; +} diff --git a/src/components/lab/LabPad/index.tsx b/src/components/lab/LabPad/index.tsx new file mode 100644 index 00000000..5d853a39 --- /dev/null +++ b/src/components/lab/LabPad/index.tsx @@ -0,0 +1,14 @@ +import React, { FC, useCallback } from 'react'; +import styles from './styles.module.scss'; +import { useHistory } from 'react-router'; +import { URLS } from '~/constants/urls'; +import { INodeComponentProps } from '~/redux/node/constants'; + +const LabPad: FC = ({ node }) => { + const history = useHistory(); + const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]); + + return
; +}; + +export { LabPad }; diff --git a/src/components/lab/LabPad/styles.module.scss b/src/components/lab/LabPad/styles.module.scss new file mode 100644 index 00000000..7869ed41 --- /dev/null +++ b/src/components/lab/LabPad/styles.module.scss @@ -0,0 +1,5 @@ +@import "~/styles/variables.scss"; + +.pad { + height: $gap; +} diff --git a/src/components/lab/LabTags/index.tsx b/src/components/lab/LabTags/index.tsx new file mode 100644 index 00000000..29a527d6 --- /dev/null +++ b/src/components/lab/LabTags/index.tsx @@ -0,0 +1,36 @@ +import React, { FC } from 'react'; +import styles from './/styles.module.scss'; +import { Placeholder } from '~/components/placeholders/Placeholder'; +import { ITag } from '~/redux/types'; +import { Tag } from '~/components/tags/Tag'; +import { Group } from '~/components/containers/Group'; + +interface IProps { + tags: ITag[]; + isLoading: boolean; +} + +const LabTags: FC = ({ tags, isLoading }) => { + if (isLoading) { + return ( +
+ + + + + + +
+ ); + } + + return ( +
+ {tags.slice(0, 10).map(tag => ( + + ))} +
+ ); +}; + +export { LabTags }; diff --git a/src/components/lab/LabTags/styles.module.scss b/src/components/lab/LabTags/styles.module.scss new file mode 100644 index 00000000..41f211dd --- /dev/null +++ b/src/components/lab/LabTags/styles.module.scss @@ -0,0 +1,10 @@ +@import "~/styles/variables.scss"; + +.tags { + display: flex; + flex-wrap: wrap; + + & > * { + margin: $gap / 2; + } +} diff --git a/src/components/lab/LabText/index.tsx b/src/components/lab/LabText/index.tsx new file mode 100644 index 00000000..ec388b13 --- /dev/null +++ b/src/components/lab/LabText/index.tsx @@ -0,0 +1,28 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { Markdown } from '~/components/containers/Markdown'; +import { INodeComponentProps } from '~/redux/node/constants'; +import { formatTextParagraphs } from '~/utils/dom'; +import { path } from 'ramda'; +import styles from './styles.module.scss'; +import { useHistory } from 'react-router'; +import { URLS } from '~/constants/urls'; + +const LabText: FC = ({ node }) => { + const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [ + node.blocks, + ]); + + const history = useHistory(); + + const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]); + + return ( + + ); +}; + +export { LabText }; diff --git a/src/components/lab/LabText/styles.module.scss b/src/components/lab/LabText/styles.module.scss new file mode 100644 index 00000000..87c83da4 --- /dev/null +++ b/src/components/lab/LabText/styles.module.scss @@ -0,0 +1,21 @@ +@import "~/styles/variables.scss"; + +.wrap { + padding: 0 $gap; + + @include tablet { + position: relative; + max-height: 50vh; + overflow: hidden; + + &::after { + content: ' '; + position: absolute; + background: linear-gradient(transparentize($lab_post_bg, 1), $lab_post_bg 80%); + bottom: 0; + left: auto; + width: 100%; + height: 100px; + } + } +} diff --git a/src/components/main/Header/index.tsx b/src/components/main/Header/index.tsx index 014e12a1..21eb7cea 100644 --- a/src/components/main/Header/index.tsx +++ b/src/components/main/Header/index.tsx @@ -1,15 +1,14 @@ -import React, { FC, useCallback, memo, useState, useEffect, useMemo } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { push as historyPush } from 'connected-react-router'; import { Link } from 'react-router-dom'; import { Logo } from '~/components/main/Logo'; import { Filler } from '~/components/containers/Filler'; -import { selectUser, selectAuthUpdates } from '~/redux/auth/selectors'; +import { selectAuthUpdates, selectUser } from '~/redux/auth/selectors'; import { Group } from '~/components/containers/Group'; import { DIALOGS } from '~/redux/modal/constants'; -import { pick } from 'ramda'; -import { path } from 'ramda'; +import { path, pick } from 'ramda'; import { UserButton } from '../UserButton'; import { Notifications } from '../Notifications'; import { URLS } from '~/constants/urls'; @@ -21,7 +20,7 @@ import * as MODAL_ACTIONS from '~/redux/modal/actions'; import * as AUTH_ACTIONS from '~/redux/auth/actions'; import { IState } from '~/redux/store'; import isBefore from 'date-fns/isBefore'; -import { Superpower } from '~/components/boris/Superpower'; +import { Authorized } from '~/components/containers/Authorized'; const mapStateToProps = (state: IState) => ({ user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)), @@ -80,7 +79,7 @@ const HeaderUnconnected: FC = memo(
- +
= memo( Π€Π›ΠžΠ£ - + ЛАБ - + = ({ node }) => { const resetSwiper = useCallback(() => { if (!controlledSwiper) return; controlledSwiper.slideTo(0, 0); - setTimeout(() => controlledSwiper.slideTo(0, 0), 300); + + // TODO: replace with working one + // setTimeout(() => controlledSwiper.slideTo(0, 0), 300); }, [controlledSwiper]); useEffect(() => { @@ -63,7 +65,7 @@ const NodeImageSwiperBlock: FC = ({ node }) => { } return ( -
+
= ({ node }) => ( -
-
- {node.title || '...'} -
-
-); - -export { NodePanelLab }; diff --git a/src/constants/api.ts b/src/constants/api.ts index c9c4c287..e968e3e8 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -52,5 +52,6 @@ export const API = { }, LAB: { NODES: `/lab/`, + STATS: '/lab/stats', }, }; diff --git a/src/containers/lab/LabGrid/index.tsx b/src/containers/lab/LabGrid/index.tsx index b6961b5f..a075ea7a 100644 --- a/src/containers/lab/LabGrid/index.tsx +++ b/src/containers/lab/LabGrid/index.tsx @@ -12,7 +12,12 @@ const LabGrid: FC = () => { return (
{nodes.map(node => ( - + ))}
); diff --git a/src/containers/lab/LabGrid/styles.module.scss b/src/containers/lab/LabGrid/styles.module.scss index 3f42c360..e58bb06d 100644 --- a/src/containers/lab/LabGrid/styles.module.scss +++ b/src/containers/lab/LabGrid/styles.module.scss @@ -4,5 +4,5 @@ display: grid; grid-auto-flow: row; grid-auto-rows: auto; - grid-row-gap: $gap; + grid-row-gap: $gap * 2; } diff --git a/src/containers/lab/LabLayout/index.tsx b/src/containers/lab/LabLayout/index.tsx index df778d28..e7ab264b 100644 --- a/src/containers/lab/LabLayout/index.tsx +++ b/src/containers/lab/LabLayout/index.tsx @@ -5,7 +5,7 @@ import { Sticky } from '~/components/containers/Sticky'; import { Container } from '~/containers/main/Container'; import { LabGrid } from '~/containers/lab/LabGrid'; import { useDispatch } from 'react-redux'; -import { labGetList } from '~/redux/lab/actions'; +import { labGetList, labGetStats } from '~/redux/lab/actions'; import { Placeholder } from '~/components/placeholders/Placeholder'; import { Grid } from '~/components/containers/Grid'; import { Group } from '~/components/containers/Group'; @@ -13,14 +13,19 @@ import { LabHero } from '~/components/lab/LabHero'; import { LabBanner } from '~/components/lab/LabBanner'; import { LabHead } from '~/components/lab/LabHead'; import { Filler } from '~/components/containers/Filler'; +import { LabStats } from '~/containers/lab/LabStats'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { selectLabList, selectLabListNodes, selectLabStatsLoading } from '~/redux/lab/selectors'; interface IProps {} const LabLayout: FC = () => { + const { is_loading } = useShallowSelect(selectLabList); const dispatch = useDispatch(); useEffect(() => { dispatch(labGetList()); + dispatch(labGetStats()); }, [dispatch]); return ( @@ -28,79 +33,13 @@ const LabLayout: FC = () => {
- +
- - - - - - - - - - - -
-
- - - -
- -
- - - - - - -
- -
-
- - - -
- - - -
- -
- -
- -
- -
- -
- - - -
-
- - - - - - -
- - - - - - - - +
diff --git a/src/containers/lab/LabLayout/styles.module.scss b/src/containers/lab/LabLayout/styles.module.scss index ee69442c..17f70529 100644 --- a/src/containers/lab/LabLayout/styles.module.scss +++ b/src/containers/lab/LabLayout/styles.module.scss @@ -4,6 +4,12 @@ display: grid; grid-template-columns: 3fr 1fr; column-gap: $gap; + + @include tablet { + grid-template-columns: 1fr; + grid-auto-flow: row; + padding: 0 $gap / 2; + } } .panel { diff --git a/src/containers/lab/LabStats/index.tsx b/src/containers/lab/LabStats/index.tsx new file mode 100644 index 00000000..18ba904c --- /dev/null +++ b/src/containers/lab/LabStats/index.tsx @@ -0,0 +1,60 @@ +import React, { FC } from 'react'; +import styles from './styles.module.scss'; +import { LabBanner } from '~/components/lab/LabBanner'; +import { Card } from '~/components/containers/Card'; +import { Group } from '~/components/containers/Group'; +import { Placeholder } from '~/components/placeholders/Placeholder'; +import { Filler } from '~/components/containers/Filler'; +import { LabHero } from '~/components/lab/LabHero'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { + selectLabStatsHeroes, + selectLabStatsLoading, + selectLabStatsTags, +} from '~/redux/lab/selectors'; +import { LabTags } from '~/components/lab/LabTags'; +import { LabHeroes } from '~/components/lab/LabHeroes'; + +interface IProps {} + +const LabStats: FC = () => { + const tags = useShallowSelect(selectLabStatsTags); + const heroes = useShallowSelect(selectLabStatsHeroes); + const isLoading = useShallowSelect(selectLabStatsLoading); + + return ( + + + +
+ + {isLoading ? ( + + ) : ( + tags.length &&
Вэги
+ )} + +
+ +
+ +
+
+
+ + {isLoading ? ( + + ) : ( + heroes.length > 0 &&
Π’Π°ΠΆΠ½Ρ‹Π΅
+ )} + +
+ +
+ +
+ + ); +}; + +export { LabStats }; diff --git a/src/containers/lab/LabStats/styles.module.scss b/src/containers/lab/LabStats/styles.module.scss new file mode 100644 index 00000000..dee75ce6 --- /dev/null +++ b/src/containers/lab/LabStats/styles.module.scss @@ -0,0 +1,25 @@ +@import "~/styles/variables.scss"; + +.title { + font: $font_14_semibold; + color: darken(white, 50%); + text-transform: uppercase; + padding: 0 $gap / 2; + padding-bottom: $gap / 2; +} + +.tags.tags { + margin: 0 -$gap / 2; +} + +.heroes { + margin-top: -$gap; +} + +.card { + @include lab_shadow; + + border-radius: $radius; + background-color: $comment_bg; + padding: $gap; +} diff --git a/src/redux/lab/actions.ts b/src/redux/lab/actions.ts index 1e1ff97a..438a8ad6 100644 --- a/src/redux/lab/actions.ts +++ b/src/redux/lab/actions.ts @@ -10,3 +10,12 @@ export const labSetList = (list: Partial) => ({ type: LAB_ACTIONS.SET_LIST, list, }); + +export const labGetStats = () => ({ + type: LAB_ACTIONS.GET_STATS, +}); + +export const labSetStats = (stats: Partial) => ({ + type: LAB_ACTIONS.SET_STATS, + stats, +}); diff --git a/src/redux/lab/api.ts b/src/redux/lab/api.ts index 5fa97bc0..16bac864 100644 --- a/src/redux/lab/api.ts +++ b/src/redux/lab/api.ts @@ -1,8 +1,10 @@ import { api, cleanResult } from '~/utils/api'; import { API } from '~/constants/api'; -import { GetLabNodesRequest, GetLabNodesResult } from '~/redux/lab/types'; +import { GetLabNodesRequest, GetLabNodesResult, GetLabStatsResult } from '~/redux/lab/types'; export const getLabNodes = ({ after }: GetLabNodesRequest) => api .get(API.LAB.NODES, { params: { after } }) .then(cleanResult); + +export const getLabStats = () => api.get(API.LAB.STATS).then(cleanResult); diff --git a/src/redux/lab/constants.ts b/src/redux/lab/constants.ts index d2e670da..0b7979b8 100644 --- a/src/redux/lab/constants.ts +++ b/src/redux/lab/constants.ts @@ -3,4 +3,7 @@ const prefix = 'LAB.'; export const LAB_ACTIONS = { GET_LIST: `${prefix}GET_LIST`, SET_LIST: `${prefix}SET_LIST`, + + GET_STATS: `${prefix}GET_STATS`, + SET_STATS: `${prefix}SET_STATS`, }; diff --git a/src/redux/lab/handlers.ts b/src/redux/lab/handlers.ts index b09812e2..f23fada1 100644 --- a/src/redux/lab/handlers.ts +++ b/src/redux/lab/handlers.ts @@ -1,5 +1,5 @@ import { LAB_ACTIONS } from '~/redux/lab/constants'; -import { labSetList } from '~/redux/lab/actions'; +import { labSetList, labSetStats } from '~/redux/lab/actions'; import { ILabState } from '~/redux/lab/types'; type LabHandler any> = ( @@ -15,6 +15,15 @@ const setList: LabHandler = (state, { list }) => ({ }, }); +const setStats: LabHandler = (state, { stats }) => ({ + ...state, + stats: { + ...state.stats, + ...stats, + }, +}); + export const LAB_HANDLERS = { [LAB_ACTIONS.SET_LIST]: setList, + [LAB_ACTIONS.SET_STATS]: setStats, }; diff --git a/src/redux/lab/index.ts b/src/redux/lab/index.ts index 56879a52..1a0bc0aa 100644 --- a/src/redux/lab/index.ts +++ b/src/redux/lab/index.ts @@ -1,6 +1,7 @@ import { createReducer } from '~/utils/reducer'; import { LAB_HANDLERS } from '~/redux/lab/handlers'; import { ILabState } from '~/redux/lab/types'; +import { INode, ITag } from '~/redux/types'; const INITIAL_STATE: ILabState = { list: { @@ -9,6 +10,12 @@ const INITIAL_STATE: ILabState = { count: 0, error: '', }, + stats: { + is_loading: false, + heroes: [], + tags: [], + error: undefined, + }, }; export default createReducer(INITIAL_STATE, LAB_HANDLERS); diff --git a/src/redux/lab/sagas.ts b/src/redux/lab/sagas.ts index 5fc48b8b..1a66b3a5 100644 --- a/src/redux/lab/sagas.ts +++ b/src/redux/lab/sagas.ts @@ -1,8 +1,8 @@ import { takeLeading, call, put } from 'redux-saga/effects'; -import { labGetList, labSetList } from '~/redux/lab/actions'; +import { labGetList, labSetList, labSetStats } from '~/redux/lab/actions'; import { LAB_ACTIONS } from '~/redux/lab/constants'; import { Unwrap } from '~/redux/types'; -import { getLabNodes } from '~/redux/lab/api'; +import { getLabNodes, getLabStats } from '~/redux/lab/api'; function* getList({ after = '' }: ReturnType) { try { @@ -16,6 +16,19 @@ function* getList({ after = '' }: ReturnType) { } } +function* getStats() { + try { + yield put(labSetStats({ is_loading: true })); + const { heroes, tags }: Unwrap = yield call(getLabStats); + yield put(labSetStats({ heroes, tags })); + } catch (error) { + yield put(labSetStats({ error: error.message })); + } finally { + yield put(labSetStats({ is_loading: false })); + } +} + export default function* labSaga() { yield takeLeading(LAB_ACTIONS.GET_LIST, getList); + yield takeLeading(LAB_ACTIONS.GET_STATS, getStats); } diff --git a/src/redux/lab/selectors.ts b/src/redux/lab/selectors.ts index 0854ac25..9c47744c 100644 --- a/src/redux/lab/selectors.ts +++ b/src/redux/lab/selectors.ts @@ -2,3 +2,7 @@ import { IState } from '~/redux/store'; export const selectLab = (state: IState) => state.lab; export const selectLabListNodes = (state: IState) => state.lab.list.nodes; +export const selectLabList = (state: IState) => state.lab.list; +export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes; +export const selectLabStatsTags = (state: IState) => state.lab.stats.tags; +export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading; diff --git a/src/redux/lab/types.ts b/src/redux/lab/types.ts index 7614807b..4a188e97 100644 --- a/src/redux/lab/types.ts +++ b/src/redux/lab/types.ts @@ -1,19 +1,36 @@ -import { IError, INode } from '~/redux/types'; +import { IError, INode, ITag } from '~/redux/types'; export type ILabState = Readonly<{ list: { is_loading: boolean; - nodes: INode[]; + nodes: ILabNode[]; count: number; error: IError; }; + stats: { + is_loading: boolean; + heroes: Partial[]; + tags: ITag[]; + error?: string; + }; }>; export type GetLabNodesRequest = { after?: string; }; +export interface ILabNode { + node: INode; + last_seen: string | null; + comment_count: number; +} + export type GetLabNodesResult = { - nodes: INode[]; + nodes: ILabNode[]; count: number; }; + +export type GetLabStatsResult = { + heroes: INode[]; + tags: ITag[]; +}; diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 8cc79869..8755a016 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -15,6 +15,11 @@ import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types'; import { EditorFiller } from '~/components/editors/EditorFiller'; import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch'; import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock'; +import { LabNodeTitle } from '~/components/lab/LabNodeTitle'; +import { LabText } from '~/components/lab/LabText'; +import { LabImage } from '~/components/lab/LabImage'; +import { LabBottomPanel } from '~/components/lab/LabBottomPanel'; +import { LabPad } from '~/components/lab/LabPad'; const prefix = 'NODE.'; export const NODE_ACTIONS = { @@ -83,6 +88,13 @@ export type INodeComponentProps = { export type INodeComponents = Record, FC>; +export const LAB_PREVIEW_LAYOUT: Record[]> = { + [NODE_TYPES.IMAGE]: [LabImage, LabPad, LabNodeTitle], + [NODE_TYPES.VIDEO]: [NodeVideoBlock, LabPad, LabNodeTitle], + [NODE_TYPES.AUDIO]: [LabPad, LabNodeTitle, LabPad, NodeAudioImageBlock, NodeAudioBlock, LabPad], + [NODE_TYPES.TEXT]: [LabPad, LabNodeTitle, LabPad, LabText, LabPad], +}; + export const NODE_HEADS: INodeComponents = { [NODE_TYPES.IMAGE]: NodeImageSwiperBlock, }; diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx index f822d726..9ace8331 100644 --- a/src/sprites/Sprites.tsx +++ b/src/sprites/Sprites.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -const Sprites: FC<{}> = () => ( +const Sprites: FC = () => ( = () => ( + + + + + + + + + + @@ -283,6 +293,22 @@ const Sprites: FC<{}> = () => ( transform="scale(0.011) translate(120, 120)" /> + + + + + + + + + + ); diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss index 3a291541..957df9be 100644 --- a/src/styles/_colors.scss +++ b/src/styles/_colors.scss @@ -2,7 +2,7 @@ // $red: #ff3344; $red: #ff3344; $yellow: #ffd60f; -$dark_blue: #3c75ff; +$dark_blue: #592071; $blue: #582cd0; $green: #00d2b9; //$green: #00503c; @@ -16,7 +16,7 @@ $primary: $red; $secondary: $wisegreen; $red_gradient: linear-gradient(165deg, $orange -50%, $red 150%); -$blue_gradient: linear-gradient(170deg, $green, $dark_blue); +$blue_gradient: linear-gradient(170deg, $blue, $dark_blue); $green_gradient: linear-gradient( 170deg, lighten(adjust_hue($wisegreen, 15deg), 10%) 0%, @@ -36,6 +36,7 @@ $main_text_color: white; $content_bg: darken($main_bg_color, 0%); $content_bg_secondary: darken($content_bg, 2%); +$lab_post_bg: lighten($content_bg, 4%); $cell_bg: lighten($main_bg_color, 0%); $card_bg: lighten($main_bg_color, 0%); diff --git a/src/styles/variables.scss b/src/styles/variables.scss index a518e5d4..0d1b25cf 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -219,3 +219,9 @@ $sidebar_border: transparentize(white, 0.95); border-radius: $radius; cursor: pointer; } + +@mixin lab_shadow { + box-shadow: transparentize(black, 0.5) 0 0 0 1px, + inset transparentize(white, 0.9) 0 1px, + lighten(black, 10%) 0 4px; +} diff --git a/src/utils/hooks/node/useNodeBlocks.ts b/src/utils/hooks/node/useNodeBlocks.ts index 823e522c..df0be6be 100644 --- a/src/utils/hooks/node/useNodeBlocks.ts +++ b/src/utils/hooks/node/useNodeBlocks.ts @@ -3,6 +3,7 @@ import { createElement, FC, useCallback, useMemo } from 'react'; import { isNil, prop } from 'ramda'; import { INodeComponentProps, + LAB_PREVIEW_LAYOUT, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES, @@ -11,11 +12,12 @@ import { // useNodeBlocks returns head, block and inline blocks of node export const useNodeBlocks = (node: INode, isLoading: boolean) => { const createNodeBlock = useCallback( - (block?: FC) => + (block?: FC, key = 0) => !isNil(block) && createElement(block, { node, isLoading, + key, }), [node, isLoading] ); @@ -35,5 +37,13 @@ export const useNodeBlocks = (node: INode, isLoading: boolean) => { [node, createNodeBlock] ); - return { head, block, inline }; + const lab = useMemo( + () => + node?.type && prop(node.type, LAB_PREVIEW_LAYOUT) + ? prop(node.type, LAB_PREVIEW_LAYOUT).map((comp, i) => createNodeBlock(comp, i)) + : undefined, + [node, createNodeBlock] + ); + + return { head, block, inline, lab }; }; diff --git a/src/utils/hooks/node/useNodeCoverImage.ts b/src/utils/hooks/node/useNodeCoverImage.ts index 5f4e7b39..62e104a9 100644 --- a/src/utils/hooks/node/useNodeCoverImage.ts +++ b/src/utils/hooks/node/useNodeCoverImage.ts @@ -10,7 +10,7 @@ export const useNodeCoverImage = (node: INode) => { dispatch(nodeSetCoverImage(node.cover)); return () => { - nodeSetCoverImage(undefined); + dispatch(nodeSetCoverImage(undefined)); }; }, [dispatch, node.cover, node.id]); }; diff --git a/src/utils/node.ts b/src/utils/node.ts index f00d006c..0e2f426e 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -16,6 +16,6 @@ export const canLikeNode = (node: Partial, user: Partial): boolean path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST; export const canStarNode = (node: Partial, user: Partial): boolean => - node.type === NODE_TYPES.IMAGE && + (node.type === NODE_TYPES.IMAGE || node.is_promoted === false) && path(['role'], user) && path(['role'], user) === USER_ROLES.ADMIN;