diff --git a/server/routes.js b/server/routes.js index 5d065baf5..cd6c57b4e 100644 --- a/server/routes.js +++ b/server/routes.js @@ -119,6 +119,11 @@ router.route('/discovery/:category').get((req, res) => { return renderAndCache({ req, res, path: '/discovery' }) }) +// 帮助中心 +router.route('/:community/help-center').get((req, res) => { + return renderAndCache({ req, res, path: '/help-center' }) +}) + // 社区主页 router.route('/:community/:thread').get((req, res) => { if ( diff --git a/size-limit.config.json b/size-limit.config.json index 6ad01d795..4f4fa9bbe 100644 --- a/size-limit.config.json +++ b/size-limit.config.json @@ -51,6 +51,10 @@ { "path": ".next/server/pages/create/article.js", "maxSize": "280 kB" + }, + { + "path": ".next/server/pages/help-center.js", + "maxSize": "280 kB" } ], "ci": { diff --git a/src/components/CollapseMenu/Group.js b/src/components/CollapseMenu/Group.js new file mode 100644 index 000000000..f8c56aa8f --- /dev/null +++ b/src/components/CollapseMenu/Group.js @@ -0,0 +1,139 @@ +import React, { useState, useRef, useEffect } from 'react' +import T from 'prop-types' + +import { findIndex } from 'ramda' + +import { ICON } from '@/config' +import { buildLog } from '@/utils' + +import Item from './Item' + +import { + Wrapper, + TagsWrapper, + Header, + ArrowIcon, + Title, + Content, + SubToggle, + SubToggleTitle, + SubTogglePrefixIcon, +} from './styles/group' + +/* eslint-disable-next-line */ +const log = buildLog('c:CollapseMenu:Group') + +const Group = ({ + title, + groupItems, + items, + activeItem, + onSelect, + maxDisplayCount, + totalToggleThrold, +}) => { + // 决定是否显示 '展示更多' 的时候参考标签总数 + const needSubToggle = + items?.length > totalToggleThrold && groupItems.length > maxDisplayCount + + const initDisplayCount = needSubToggle ? maxDisplayCount : groupItems.length + + const [isFolderOpen, toggleFolder] = useState(true) + const [curDisplayCount, setCurDisplayCount] = useState(initDisplayCount) + + const sortedItems = groupItems // sortByColor(groupItems) + + const isActiveTagInFolder = + findIndex((item) => item.id === activeItem.id, groupItems) >= 0 + + const subToggleRef = useRef(null) + // 当选中的 Tag 被折叠在展示更多里面时,将其展开 + useEffect(() => { + if (subToggleRef && isActiveTagInFolder) { + setCurDisplayCount(groupItems.length) + } + }, [subToggleRef, isActiveTagInFolder, groupItems]) + + return ( + +
{ + toggleFolder(!isFolderOpen) + + // 当关闭 Folder 的时候,如果当前 Folder 没有被激活的 Tag, 那么就回到折叠状态 + // 如果有,那么保持原来的状态 + if (isFolderOpen && !isActiveTagInFolder) { + setCurDisplayCount(maxDisplayCount) + } + }} + > + + {title} +
+ + + + {sortedItems.slice(0, curDisplayCount).map((item) => ( + + ))} + + {needSubToggle && ( + { + setCurDisplayCount( + curDisplayCount === maxDisplayCount + ? groupItems.length + : maxDisplayCount, + ) + }} + > + + + {curDisplayCount === maxDisplayCount ? '展开更多' : '收起'} + + + )} + +
+ ) +} + +Group.propTypes = { + // title, groupItems, items, activeItem, onSelect + title: T.string, + groupItems: T.arrayOf( + T.shape({ + id: T.number, + title: T.string, + }), + ).isRequired, + items: T.arrayOf( + T.shape({ + id: T.number, + title: T.string, + }), + ).isRequired, + activeItem: T.shape({ + id: T.number, + title: T.string, + }).isRequired, + maxDisplayCount: T.number.isRequired, + totalToggleThrold: T.number.isRequired, + onSelect: T.func.isRequired, +} + +Group.defaultProps = { + title: '', +} + +export default React.memo(Group) diff --git a/src/components/CollapseMenu/Item.js b/src/components/CollapseMenu/Item.js new file mode 100644 index 000000000..4c00d11e7 --- /dev/null +++ b/src/components/CollapseMenu/Item.js @@ -0,0 +1,16 @@ +import React from 'react' + +import { Wrapper, Title } from './styles/item' + +// const Item = ({ item, active, activeId, onSelect }) => { +const Item = ({ item, active, onSelect }) => { + return ( + + onSelect(item)}> + {item.title} + + + ) +} + +export default Item diff --git a/src/components/CollapseMenu/index.js b/src/components/CollapseMenu/index.js new file mode 100755 index 000000000..9cf56e6cf --- /dev/null +++ b/src/components/CollapseMenu/index.js @@ -0,0 +1,137 @@ +/* + * + * CollapseMenu + * + */ + +import React from 'react' +import T from 'prop-types' +import { keys } from 'ramda' + +import { buildLog, groupByKey } from '@/utils' + +import Group from './Group' + +import { Wrapper } from './styles' + +const MAX_DISPLAY_COUNT = 5 +const TOTAL_TOGGLE_THROLD = 8 // 15 + +/* eslint-disable-next-line */ +const log = buildLog('c:CollapseMenu:index') + +const defaultActiveItem = { id: 2 } +const defaultItems = [ + { + id: 1, + title: 'coderplanets 是什么?', + group: '基础问答', + }, + { + id: 2, + title: '持续部署项目实践', + group: '基础问答', + }, + { + id: 3, + title: 'coderplanets 是什么 3?', + group: '基础问答', + }, + + { + id: 4, + title: 'coderplanets 是什么 4?', + group: '进阶问答', + }, + { + id: 5, + title: 'coderplanets 是什么 5?', + group: '进阶问答', + }, + { + id: 6, + title: 'coderplanets 是什么 6?', + group: '进阶问答', + }, + { + id: 7, + title: 'coderplanets 是什么 7?', + group: '进阶问答', + }, + { + id: 8, + title: 'coderplanets 是什么 8?', + group: '进阶问答', + }, + { + id: 9, + title: 'coderplanets 是什么 9?', + group: '进阶问答', + }, + { + id: 10, + title: 'coderplanets 是什么 10?', + group: '进阶问答', + }, +] + +const CollapseMenu = ({ + testId, + items, + activeItem, + onSelect, + maxDisplayCount, + totalToggleThrold, +}) => { + const groupedItems = groupByKey(items, 'group') + const groupsKeys = keys(groupedItems) + + return ( + + {groupsKeys.map((groupKey) => ( + + ))} + + ) +} + +CollapseMenu.propTypes = { + testId: T.string, + items: T.arrayOf( + T.shape({ + id: T.number, + title: T.string, + group: T.string, + }), + ), // .isRequired, + activeItem: T.shape({ + id: T.number, + title: T.string, + group: T.string, + }), // .isRequired, + maxDisplayCount: T.number, + totalToggleThrold: T.number, + onSelect: T.func, +} + +CollapseMenu.defaultProps = { + testId: 'collapse-menu', + items: defaultItems, + activeItem: defaultActiveItem, + // default display count in each group, the remaining part will be folded + maxDisplayCount: MAX_DISPLAY_COUNT, + // if items count < than this, will not be folded in each group + totalToggleThrold: TOTAL_TOGGLE_THROLD, + onSelect: console.log, +} + +export default React.memo(CollapseMenu) diff --git a/src/components/CollapseMenu/styles/group.js b/src/components/CollapseMenu/styles/group.js new file mode 100644 index 000000000..46afb4f75 --- /dev/null +++ b/src/components/CollapseMenu/styles/group.js @@ -0,0 +1,67 @@ +import styled from 'styled-components' + +import Img from '@/Img' +import { css, theme } from '@/utils' + +export const Wrapper = styled.div`` + +export const TagsWrapper = styled.div`` + +export const Header = styled.div` + ${css.flex('align-center')}; + margin-bottom: 8px; + margin-left: 3px; + &:hover { + cursor: pointer; + /* opacity: 0.65; */ + } +` +export const ArrowIcon = styled(Img)` + fill: ${theme('tags.text')}; + ${css.size(16)}; + opacity: 0.5; + transform: ${({ isOpen }) => (isOpen ? 'rotate(270deg)' : 'rotate(180deg)')}; + transition: transform 0.5s; + ${Header}:hover & { + opacity: 0.65; + } +` +export const Title = styled.div` + color: ${theme('tags.text')}; + opacity: 0.5; + margin-left: 4px; + font-size: 14px; + margin-right: 8px; + ${css.cutFrom('85px')}; + + ${Header}:hover & { + opacity: 0.65; + } +` +export const Content = styled.div` + display: ${({ isOpen }) => (isOpen ? 'block' : 'none')}; + width: 100%; + margin-bottom: 15px; +` +export const SubToggle = styled.div` + ${css.flex('align-center')}; + margin-left: 5px; + opacity: 0.5; + + &:hover { + opacity: 0.8; + cursor: pointer; + } +` +export const SubToggleTitle = styled.div` + color: ${theme('tags.text')}; + font-size: 12px; + margin-left: 5px; + padding: 2px; + border-radius: 5px; +` +export const SubTogglePrefixIcon = styled(Img)` + fill: ${theme('tags.text')}; + ${css.size(14)}; + transform: rotate(90deg); +` diff --git a/src/components/CollapseMenu/styles/index.js b/src/components/CollapseMenu/styles/index.js new file mode 100755 index 000000000..46afb4f75 --- /dev/null +++ b/src/components/CollapseMenu/styles/index.js @@ -0,0 +1,67 @@ +import styled from 'styled-components' + +import Img from '@/Img' +import { css, theme } from '@/utils' + +export const Wrapper = styled.div`` + +export const TagsWrapper = styled.div`` + +export const Header = styled.div` + ${css.flex('align-center')}; + margin-bottom: 8px; + margin-left: 3px; + &:hover { + cursor: pointer; + /* opacity: 0.65; */ + } +` +export const ArrowIcon = styled(Img)` + fill: ${theme('tags.text')}; + ${css.size(16)}; + opacity: 0.5; + transform: ${({ isOpen }) => (isOpen ? 'rotate(270deg)' : 'rotate(180deg)')}; + transition: transform 0.5s; + ${Header}:hover & { + opacity: 0.65; + } +` +export const Title = styled.div` + color: ${theme('tags.text')}; + opacity: 0.5; + margin-left: 4px; + font-size: 14px; + margin-right: 8px; + ${css.cutFrom('85px')}; + + ${Header}:hover & { + opacity: 0.65; + } +` +export const Content = styled.div` + display: ${({ isOpen }) => (isOpen ? 'block' : 'none')}; + width: 100%; + margin-bottom: 15px; +` +export const SubToggle = styled.div` + ${css.flex('align-center')}; + margin-left: 5px; + opacity: 0.5; + + &:hover { + opacity: 0.8; + cursor: pointer; + } +` +export const SubToggleTitle = styled.div` + color: ${theme('tags.text')}; + font-size: 12px; + margin-left: 5px; + padding: 2px; + border-radius: 5px; +` +export const SubTogglePrefixIcon = styled(Img)` + fill: ${theme('tags.text')}; + ${css.size(14)}; + transform: rotate(90deg); +` diff --git a/src/components/CollapseMenu/styles/item.js b/src/components/CollapseMenu/styles/item.js new file mode 100644 index 000000000..060401801 --- /dev/null +++ b/src/components/CollapseMenu/styles/item.js @@ -0,0 +1,30 @@ +import styled from 'styled-components' + +import { theme, css } from '@/utils' + +export const Wrapper = styled.div` + ${css.flex('align-center')}; + margin-bottom: 3px; + padding: 5px; + padding-left: 10px; + max-width: 200px; + border-radius: 5px; + + background: ${({ active }) => (!active ? 'transparent' : '#0e303d')}; +` +export const Title = styled.div` + color: ${theme('tags.text')}; + ${css.cutFrom('200px')}; + font-size: 14px; + opacity: 0.9; + font-weight: ${({ active }) => (active ? 'bold' : 'normal')}; + opacity: ${({ active }) => (active ? 1 : 0.9)}; + + &:hover { + cursor: pointer; + opacity: 1; + font-weight: bold; + } + + transition: all 0.2s; +` diff --git a/src/components/CollapseMenu/tests/index.test.js b/src/components/CollapseMenu/tests/index.test.js new file mode 100755 index 000000000..2ca10d313 --- /dev/null +++ b/src/components/CollapseMenu/tests/index.test.js @@ -0,0 +1,10 @@ +// import React from 'react' +// import { shallow } from 'enzyme' + +// import CollapseMenu from '../index' + +describe('TODO ', () => { + it('Expect to have unit tests specified', () => { + expect(true).toEqual(true) + }) +}) diff --git a/src/components/CommunityStatesPad/SubscribedTitle.js b/src/components/CommunityStatesPad/SubscribedTitle.js deleted file mode 100755 index 79d2f1c5f..000000000 --- a/src/components/CommunityStatesPad/SubscribedTitle.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' - -import { NumberTitle } from './styles' - -const SubscribedTitle = ({ viewerHasSubscribed }) => ( - - {viewerHasSubscribed ? ( - 已加入 - ) : ( - 加入 - )} - -) - -export default React.memo(SubscribedTitle) diff --git a/src/components/CommunityStatesPad/index.js b/src/components/CommunityStatesPad/index.js index e4507e2cf..110e93582 100755 --- a/src/components/CommunityStatesPad/index.js +++ b/src/components/CommunityStatesPad/index.js @@ -11,7 +11,6 @@ import { useDevice } from '@/hooks' import { buildLog } from '@/utils' import Charger from '@/components/Charger' -import SubscribedTitle from './SubscribedTitle' import NumberGroup from './NumberGroup' import { @@ -49,9 +48,7 @@ const CommunityStatesPad = ({ return ( - {!isMobile && ( - - )} + {!isMobile && 成员} { - 常见问题 + 帮助中心 diff --git a/src/components/FaqPeekList/index.js b/src/components/FaqPeekList/index.js index 199c4b867..defe84454 100755 --- a/src/components/FaqPeekList/index.js +++ b/src/components/FaqPeekList/index.js @@ -4,20 +4,15 @@ * */ -import React from 'react' +import React, { useState, useEffect } from 'react' import T from 'prop-types' import { ICON } from '@/config' import { buildLog } from '@/utils' -import Post from './Post' -import { - Wrapper, - ArrowIcon, - ContentWrapper, - Title, - ListWrapper, -} from './styles' +import LinksCard from '@/components/LinksCard' + +import { Wrapper, ArrowIcon, ContentWrapper } from './styles' /* eslint-disable-next-line */ const log = buildLog('c:FaqPeekList:index') @@ -39,24 +34,37 @@ const items = [ id: '3', title: '后续会有更多的作品吗', }, - { - id: '4', - title: '太喜欢这个社区了,不知道', - }, ] const FaqPeekList = ({ active }) => { + const [showContent, setShowContent] = useState(false) + + // wait for expand animation to finish + useEffect(() => { + active ? setTimeout(() => setShowContent(true), 150) : setShowContent(false) + }, [active]) + return ( {active && } - - 常见问题: + + - - {items.map((item) => ( - - ))} - + ) diff --git a/src/components/FaqPeekList/styles/index.js b/src/components/FaqPeekList/styles/index.js index 7517ca19e..a9be03187 100755 --- a/src/components/FaqPeekList/styles/index.js +++ b/src/components/FaqPeekList/styles/index.js @@ -1,44 +1,53 @@ import styled from 'styled-components' import Img from '@/Img' -import { css, theme } from '@/utils' +import { theme } from '@/utils' export const Wrapper = styled.div.attrs((props) => ({ 'data-test-id': props.testId, }))` position: relative; - max-height: ${({ active }) => (active ? '180px' : '0')}; - border: ${({ active }) => (active ? '1px solid' : 'none')}; - border-left: none; - border-right: none; - border-color: ${({ active }) => (active ? '#126682' : 'transparent')}; + max-height: ${({ active }) => (active ? '220px' : '0')}; + height: 220px; margin-top: ${({ active }) => (active ? '16px' : '0')}; margin-bottom: ${({ active }) => (active ? '18px' : '0')}; - background: #002b35; padding: ${({ active }) => (active ? '15px' : '0')}; - transition: max-height 0.25s; + + background: #0d2c38; + border-left: 2px solid; + border-right: 2px solid; + border-left-color: ${theme('content.bg')}; + border-right-color: ${theme('content.bg')}; + + margin-left: -25px; + /* conent padding-left(25px) + padding-right(24px) */ + width: calc(100% + 49px); + border-radius: 3px; + + transition: max-height 0.2s; ` export const ArrowIcon = styled(Img)` - fill: #126682; + fill: #0d2c38; position: absolute; top: -14px; - right: 22px; + right: 45px; height: 18px; width: 18px; transform: rotate(-90deg); ` export const ContentWrapper = styled.div` - display: ${({ active }) => (active ? 'block' : 'none')}; - /* opacity: ${({ active }) => (active ? '1' : '0')}; */ - /* transition: opacity 0.35s; */ -` -export const Title = styled.div` - color: ${theme('banner.title')}; - font-size: 14px; - font-weight: bold; -` -export const ListWrapper = styled.div` - ${css.flex()}; - flex-wrap: wrap; - margin-top: 15px; + display: ${({ active }) => (active ? 'flex' : 'none')}; + align-items: center; + justify-content: flex-start; + height: 100%; + background: ; ` +// export const GroupWrapper = styled.div` +// width: 280px; +// height: 180px; +// background: #0e303c; +// border-radius: 5px; +// padding: 10px; +// margin-right: 14px; +// border: 1px solid #0f4556; +// ` diff --git a/src/components/FaqPeekList/Post.js b/src/components/LinksCard/Item.js similarity index 53% rename from src/components/FaqPeekList/Post.js rename to src/components/LinksCard/Item.js index 412dd90c7..6a5c34c9f 100644 --- a/src/components/FaqPeekList/Post.js +++ b/src/components/LinksCard/Item.js @@ -10,18 +10,15 @@ import T from 'prop-types' import { ICON } from '@/config' import { buildLog } from '@/utils' -import { Wrapper, Dot, Title, Reaction, Icon, Count } from './styles/post' +import { Wrapper, Title, Reaction, Icon, Count } from './styles/item' /* eslint-disable-next-line */ -const log = buildLog('c:Post:index') +const log = buildLog('c:LinksCard:Item') -const Post = ({ item }) => { +const Item = ({ item, onSelect }) => { return ( - - - <Dot /> - {item.title} - + onSelect(item)}> + {item.title} 28 @@ -30,12 +27,13 @@ const Post = ({ item }) => { ) } -Post.propTypes = { +Item.propTypes = { item: T.shape({ title: T.string, }).isRequired, + onSelect: T.func.isRequired, } -Post.defaultProps = {} +Item.defaultProps = {} -export default React.memo(Post) +export default React.memo(Item) diff --git a/src/components/LinksCard/index.js b/src/components/LinksCard/index.js new file mode 100755 index 000000000..0dcccd837 --- /dev/null +++ b/src/components/LinksCard/index.js @@ -0,0 +1,79 @@ +/* + * + * LinksCard + * + */ + +import React from 'react' +import T from 'prop-types' + +import { buildLog } from '@/utils' + +import { ArrowButton } from '@/components/Buttons' + +import Item from './Item' +import { Wrapper, Header, Title, ListWrapper, MoreWrapper } from './styles' + +/* eslint-disable-next-line */ +const log = buildLog('c:LinksCard:index') + +const LinksCard = ({ + testId, + title, + items, + onSelect, + mLeft, + mRight, + mBottom, +}) => { + return ( + +
+ {title} +
+ + {items.map((item) => ( + + ))} + + { + onSelect() + }} + > + 更多 + + + +
+ ) +} + +LinksCard.propTypes = { + testId: T.string, + title: T.string, + items: T.arrayOf( + T.shape({ + id: T.number, + title: T.string, + }), + ), + onSelect: T.func.isRequired, + mLeft: T.number, + mRight: T.number, + mBottom: T.number, +} + +LinksCard.defaultProps = { + testId: 'links-card', + title: '', + items: [], + + mLeft: 14, + mRight: 12, + mBottom: 40, +} + +export default React.memo(LinksCard) diff --git a/src/components/LinksCard/styles/index.js b/src/components/LinksCard/styles/index.js new file mode 100755 index 000000000..f76ce5638 --- /dev/null +++ b/src/components/LinksCard/styles/index.js @@ -0,0 +1,38 @@ +import styled from 'styled-components' + +import { css, theme } from '@/utils' + +export const Wrapper = styled.div` + width: 280px; + min-height: 180px; + border-radius: 5px; + padding: 10px; + margin-left: ${({ mLeft }) => `${mLeft}px`}; + margin-right: ${({ mRight }) => `${mRight}px`}; + margin-bottom: ${({ mBottom }) => `${mBottom}px`}; + /* border-bottom: 1px solid #0f4556; */ +` +export const Header = styled.div`` + +export const Title = styled.span` + color: ${theme('banner.title')}; + font-size: 14px; + font-weight: bold; + /* padding-bottom: 5px; + border-bottom: 1px solid; + border-bottom-color: ${theme('banner.desc')}; */ +` +export const ListWrapper = styled.div` + ${css.flexColumn()}; + margin-top: 15px; +` +export const MoreWrapper = styled.div` + opacity: 0.5; + margin-top: 2px; + + &:hover { + opacity: 1; + } + + transition: opacity 0.25s; +` diff --git a/src/components/FaqPeekList/styles/post.js b/src/components/LinksCard/styles/item.js similarity index 70% rename from src/components/FaqPeekList/styles/post.js rename to src/components/LinksCard/styles/item.js index 13b504430..1069b7cf7 100644 --- a/src/components/FaqPeekList/styles/post.js +++ b/src/components/LinksCard/styles/item.js @@ -5,31 +5,17 @@ import { css, theme } from '@/utils' export const Wrapper = styled.div` ${css.flex('align-center', 'justify-between')}; - min-width: 50%; margin-bottom: 8px; - padding-right: 25px; - - &:nth-child(even) { - padding-right: 0; - padding-left: 15px; - } -` -export const Dot = styled.div` - background: ${theme('banner.desc')}; - width: 4px; - height: 4px; - border-radius: 50%; - margin-right: 5px; ` export const Title = styled.div` - ${css.flex('align-center')}; + /* ${css.flex('align-center')}; */ + ${css.cutFrom('200px')}; color: ${theme('banner.desc')}; ${Wrapper}:hover & { color: ${theme('banner.title')}; cursor: pointer; } - transition: all 0.25s; ` export const Reaction = styled.div` ${css.flex('align-center')}; diff --git a/src/components/LinksCard/tests/index.test.js b/src/components/LinksCard/tests/index.test.js new file mode 100755 index 000000000..536e1e255 --- /dev/null +++ b/src/components/LinksCard/tests/index.test.js @@ -0,0 +1,10 @@ +// import React from 'react' +// import { shallow } from 'enzyme' + +// import LinksCard from '../index' + +describe('TODO ', () => { + it('Expect to have unit tests specified', () => { + expect(true).toEqual(true) + }) +}) diff --git a/src/containers/content/HelpCenterContent/Cover.js b/src/containers/content/HelpCenterContent/Cover.js new file mode 100644 index 000000000..9e6bfd1eb --- /dev/null +++ b/src/containers/content/HelpCenterContent/Cover.js @@ -0,0 +1,59 @@ +import React from 'react' + +import LinksCard from '@/components/LinksCard' + +import { Wrapper } from './styles/cover' +import { gotoDetail } from './logic' + +const items = [ + { + id: '0', + title: '这是一个什么社区?', + }, + { + id: '1', + title: 'Wix Bookings: Tips for Marketing', + }, + { + id: '2', + title: '在哪里可以下载到 iOS 版本的安装包?', + }, + { + id: '3', + title: '后续会有更多的作品吗', + }, +] + +const Cover = () => { + return ( + + gotoDetail(item)} + /> + gotoDetail(item)} + /> + gotoDetail(item)} + /> + gotoDetail(item)} + /> + gotoDetail(item)} + /> + + ) +} + +export default Cover diff --git a/src/containers/content/HelpCenterContent/Detail.js b/src/containers/content/HelpCenterContent/Detail.js new file mode 100644 index 000000000..8a2472f56 --- /dev/null +++ b/src/containers/content/HelpCenterContent/Detail.js @@ -0,0 +1,18 @@ +import React from 'react' + +import CollapseMenu from '@/components/CollapseMenu' + +import { Wrapper, ArticleWrapper, MenuWrapper } from './styles/detail' + +const Detail = () => { + return ( + + 帮助详情 + + + + + ) +} + +export default Detail diff --git a/src/containers/content/HelpCenterContent/Digest.js b/src/containers/content/HelpCenterContent/Digest.js new file mode 100644 index 000000000..fc62af47e --- /dev/null +++ b/src/containers/content/HelpCenterContent/Digest.js @@ -0,0 +1,33 @@ +import React from 'react' + +import { + Wrapper, + InnerWrapper, + BreadCrumbs, + Community, + CommunityLogo, + CommunityTitle, + Slash, + HelpTitle, +} from './styles/digest' + +const Digest = ({ metric, community }) => { + return ( + + + + + + + {community.title} + + + / + 帮助中心 + + + + ) +} + +export default Digest diff --git a/src/containers/content/HelpCenterContent/constant.js b/src/containers/content/HelpCenterContent/constant.js new file mode 100644 index 000000000..05296d3da --- /dev/null +++ b/src/containers/content/HelpCenterContent/constant.js @@ -0,0 +1,6 @@ +export const VIEW = { + COVER: 'cover', + DETAIL: 'detail', +} + +export const holder = 1 diff --git a/src/containers/content/HelpCenterContent/index.js b/src/containers/content/HelpCenterContent/index.js new file mode 100755 index 000000000..425e5c5ec --- /dev/null +++ b/src/containers/content/HelpCenterContent/index.js @@ -0,0 +1,57 @@ +// + +/* + * + * HelpCenterContent + * + */ + +import React from 'react' +import T from 'prop-types' +import { values } from 'ramda' + +import { METRIC } from '@/constant' +import { pluggedIn, buildLog } from '@/utils' + +import Cover from './Cover' +import Detail from './Detail' +import Digest from './Digest' + +import { VIEW } from './constant' + +import { Wrapper, ContentWrapper } from './styles' +import { useInit } from './logic' + +/* eslint-disable-next-line */ +const log = buildLog('C:HelpCenterContent') + +const HelpCenterContentContainer = ({ + helpCenterContent: store, + testId, + metric, +}) => { + useInit(store) + const { view, curCommunity } = store + + return ( + + + + {view === VIEW.COVER ? : } + + + ) +} + +HelpCenterContentContainer.propTypes = { + helpCenterContent: T.any.isRequired, + testId: T.string, + metric: T.oneOf(values(METRIC)), +} + +HelpCenterContentContainer.defaultProps = { + testId: 'help-center-content', + metric: METRIC.HELP_CENTER, +} + +export default pluggedIn(HelpCenterContentContainer) diff --git a/src/containers/content/HelpCenterContent/logic.js b/src/containers/content/HelpCenterContent/logic.js new file mode 100755 index 000000000..5c757c496 --- /dev/null +++ b/src/containers/content/HelpCenterContent/logic.js @@ -0,0 +1,30 @@ +import { useEffect } from 'react' +// import { } from 'ramda' + +import { buildLog } from '@/utils' +// import S from './service' +import { VIEW } from './constant' + +let store = null + +/* eslint-disable-next-line */ +const log = buildLog('L:HelpCenterContent') + +/** + * goto detail help-center article + */ +export const gotoDetail = () => { + store.mark({ view: VIEW.DETAIL }) +} + +// ############################### +// init & uninit handlers +// ############################### + +export const useInit = (_store) => { + useEffect(() => { + store = _store + log('useInit: ', store) + // return () => store.reset() + }, [_store]) +} diff --git a/src/containers/content/HelpCenterContent/schema.js b/src/containers/content/HelpCenterContent/schema.js new file mode 100755 index 000000000..0035db982 --- /dev/null +++ b/src/containers/content/HelpCenterContent/schema.js @@ -0,0 +1,23 @@ +import gql from 'graphql-tag' + +const simpleMutation = gql` + mutation($id: ID!) { + post(id: $id) { + id + } + } +` +const simpleQuery = gql` + query($filter: filter!) { + post(id: $id) { + id + } + } +` + +const schema = { + simpleMutation, + simpleQuery, +} + +export default schema diff --git a/src/containers/content/HelpCenterContent/store.js b/src/containers/content/HelpCenterContent/store.js new file mode 100755 index 000000000..cb8b1a10b --- /dev/null +++ b/src/containers/content/HelpCenterContent/store.js @@ -0,0 +1,33 @@ +/* + * HelpCenterContent store + * + */ + +import { types as T, getParent } from 'mobx-state-tree' +import { values } from 'ramda' + +import { markStates, buildLog, stripMobx } from '@/utils' + +import { VIEW } from './constant' + +/* eslint-disable-next-line */ +const log = buildLog('S:HelpCenterContent') + +const HelpCenterContent = T.model('HelpCenterContent', { + view: T.optional(T.enumeration(values(VIEW)), VIEW.COVER), +}) + .views((self) => ({ + get root() { + return getParent(self) + }, + get curCommunity() { + return stripMobx(self.root.viewing.community) + }, + })) + .actions((self) => ({ + mark(sobj) { + markStates(sobj, self) + }, + })) + +export default HelpCenterContent diff --git a/src/containers/content/HelpCenterContent/styles/cover.js b/src/containers/content/HelpCenterContent/styles/cover.js new file mode 100644 index 000000000..2ef478d16 --- /dev/null +++ b/src/containers/content/HelpCenterContent/styles/cover.js @@ -0,0 +1,13 @@ +import styled from 'styled-components' + +import { css, theme } from '@/utils' + +export const Wrapper = styled.div` + ${css.flex()}; + background: ${theme('thread.bg')}; + flex-wrap: wrap; + padding: 80px 30px; + padding-right: 10px; +` + +export const holder = 1 diff --git a/src/containers/content/HelpCenterContent/styles/detail.js b/src/containers/content/HelpCenterContent/styles/detail.js new file mode 100644 index 000000000..a3a5844d7 --- /dev/null +++ b/src/containers/content/HelpCenterContent/styles/detail.js @@ -0,0 +1,22 @@ +import styled from 'styled-components' + +import { css, theme } from '@/utils' + +export const Wrapper = styled.div` + ${css.flex()}; + width: 100%; +` +export const ArticleWrapper = styled.div` + color: ${theme('thread.articleDigest')}; + width: 100%; + padding: 30px; + flex-grow: 1; + background: ${theme('thread.bg')}; + min-height: 70vh; +` +export const MenuWrapper = styled.div` + ${css.flex('justify-end')}; + width: 300px; + margin-left: 30px; + margin-top: 20px; +` diff --git a/src/containers/content/HelpCenterContent/styles/digest.js b/src/containers/content/HelpCenterContent/styles/digest.js new file mode 100644 index 000000000..4d15a7fcb --- /dev/null +++ b/src/containers/content/HelpCenterContent/styles/digest.js @@ -0,0 +1,52 @@ +import styled from 'styled-components' + +import Img from '@/Img' +import { css, theme } from '@/utils' + +export const Wrapper = styled.div` + ${css.flex('justify-center')}; + width: 100%; + height: 100px; + background: ${theme('banner.bg')}; + ${({ metric }) => css.fitPageWidth(metric)}; +` +export const InnerWrapper = styled.div` + ${css.flex('align-center')}; + width: 100%; + height: 100%; + ${({ metric }) => css.fitContentWidth(metric)}; + + /* tmp */ + padding-left: 10px; + color: ${theme('thread.articleTitle')}; + font-size: 18px; +` +export const BreadCrumbs = styled.div` + ${css.flex('align-center')}; +` +export const Community = styled.div` + ${css.flex('align-center')}; +` +export const CommunityLogo = styled(Img)` + ${css.size(28)}; + margin-right: 12px; +` +export const CommunityTitle = styled.a` + color: ${theme('banner.desc')}; + text-decoration: none; + font-size: 18px; + + &:hover { + text-decoration: underline; + } +` +export const Slash = styled.div` + color: ${theme('banner.desc')}; + margin-left: 8px; + margin-right: 8px; +` +export const HelpTitle = styled.div` + color: ${theme('banner.title')}; + font-weight: bold; + font-size: 16px; +` diff --git a/src/containers/content/HelpCenterContent/styles/index.js b/src/containers/content/HelpCenterContent/styles/index.js new file mode 100755 index 000000000..c8c066a32 --- /dev/null +++ b/src/containers/content/HelpCenterContent/styles/index.js @@ -0,0 +1,23 @@ +import styled from 'styled-components' + +import { css } from '@/utils' + +export const Wrapper = styled.div.attrs((props) => ({ + 'data-test-id': props.testId, +}))` + ${css.flexColumn('align-center')}; + width: 100%; + min-height: 80vh; +` +export const ContentWrapper = styled.div` + ${css.flex('justify-center')}; + ${({ metric }) => css.fitContentWidth(metric)}; + width: 100%; + margin-top: 20px; +` +export const CoverWrapper = styled.div` + ${css.flex()}; + flex-wrap: wrap; + padding: 80px 30px; + padding-right: 10px; +` diff --git a/src/containers/content/HelpCenterContent/tests/index.test.js b/src/containers/content/HelpCenterContent/tests/index.test.js new file mode 100755 index 000000000..bad8b9388 --- /dev/null +++ b/src/containers/content/HelpCenterContent/tests/index.test.js @@ -0,0 +1,10 @@ +// import React from 'react' +// import { shallow } from 'enzyme' + +// import HelpCenterContent from '../index' + +describe('TODO ', () => { + it('Expect to have unit tests specified', () => { + expect(true).toEqual(true) + }) +}) diff --git a/src/containers/content/HelpCenterContent/tests/store.test.js b/src/containers/content/HelpCenterContent/tests/store.test.js new file mode 100755 index 000000000..d9bfc4b65 --- /dev/null +++ b/src/containers/content/HelpCenterContent/tests/store.test.js @@ -0,0 +1,10 @@ +/* + * HelpCenterContent store test + * + */ + +// import HelpCenterContent from '../index' + +it('TODO: store test HelpCenterContent', () => { + expect(1 + 1).toBe(2) +}) diff --git a/src/containers/thread/PostsThread/index.js b/src/containers/thread/PostsThread/index.js index 7fcf59777..fa2375bc4 100755 --- a/src/containers/thread/PostsThread/index.js +++ b/src/containers/thread/PostsThread/index.js @@ -33,7 +33,6 @@ import { FilterWrapper, BadgeWrapper, PublisherWrapper, - StickyHolder, } from './styles' import { @@ -190,7 +189,6 @@ const PostsThreadContainer = ({ postsThread: store }) => { active={activeTagData} /> - )} diff --git a/src/containers/thread/PostsThread/styles/index.js b/src/containers/thread/PostsThread/styles/index.js index d9163c2ee..6ad2b6738 100755 --- a/src/containers/thread/PostsThread/styles/index.js +++ b/src/containers/thread/PostsThread/styles/index.js @@ -40,7 +40,3 @@ export const FilterWrapper = styled.div` ${css.media.mobile`margin-bottom: 4px;`}; margin-left: -5px; ` -export const StickyHolder = styled.div` - /* align the footer */ - height: 35px; -` diff --git a/src/containers/unit/Header/store.js b/src/containers/unit/Header/store.js index 04c154d01..33149c65b 100755 --- a/src/containers/unit/Header/store.js +++ b/src/containers/unit/Header/store.js @@ -41,6 +41,7 @@ const HeaderStore = T.model('HeaderStore', { METRIC.MEMBERSHIP, METRIC.USER, METRIC.COMMUNITY_EDITOR, + METRIC.HELP_CENTER, ]) }, get leftOffset() { diff --git a/src/containers/unit/TagsBar/DesktopView/GobackTag.js b/src/containers/unit/TagsBar/DesktopView/GobackTag.js index 148448f69..a8a082d41 100644 --- a/src/containers/unit/TagsBar/DesktopView/GobackTag.js +++ b/src/containers/unit/TagsBar/DesktopView/GobackTag.js @@ -3,13 +3,12 @@ import React from 'react' import { ICON } from '@/config' import { Wrapper, TagIcon, TagTitle } from '../styles/desktop_view/goback_tag' -import { onTagSelect } from '../logic' const GobackTag = ({ onSelect }) => { const emptytag = { id: '', title: '', color: '' } return ( - onTagSelect(emptytag, onSelect)}> + onSelect(emptytag)}> 全部标签 diff --git a/src/containers/unit/TagsBar/DesktopView/TagItem.js b/src/containers/unit/TagsBar/DesktopView/TagItem.js index f3401eea6..1f041af11 100644 --- a/src/containers/unit/TagsBar/DesktopView/TagItem.js +++ b/src/containers/unit/TagsBar/DesktopView/TagItem.js @@ -12,8 +12,6 @@ import { CountInfoWrapper, } from '../styles/desktop_view/tag_item' -import { onTagSelect } from '../logic' - const TagItem = ({ tag, active, activeId, inline, onSelect }) => { return ( @@ -24,11 +22,7 @@ const TagItem = ({ tag, active, activeId, inline, onSelect }) => { inline={inline} /> - onTagSelect(tag, onSelect)} - > + onSelect(tag)}> {Trans(tag.title)} diff --git a/src/containers/unit/TagsBar/DesktopView/index.js b/src/containers/unit/TagsBar/DesktopView/index.js index a1cb71644..9834c9f1a 100644 --- a/src/containers/unit/TagsBar/DesktopView/index.js +++ b/src/containers/unit/TagsBar/DesktopView/index.js @@ -9,14 +9,14 @@ import T from 'prop-types' import { keys } from 'ramda' import { THREAD, TOPIC } from '@/constant' -import { buildLog, pluggedIn, groupByKey } from '@/utils' +import { buildLog, pluggedIn } from '@/utils' import GobackTag from './GobackTag' import Folder from './Folder' import { Wrapper } from '../styles/desktop_view' -import { useInit } from '../logic' +import { useInit, onTagSelect } from '../logic' /* eslint-disable-next-line */ const log = buildLog('C:TagsBar') @@ -29,33 +29,30 @@ const TagsBarContainer = ({ onSelect, }) => { useInit(store, thread, topic, active) - const { tagsData, activeTagData } = store - // const tagsByGroup = groupByKey(tagsData, 'group') - const tagsByGroup = groupByKey( - tagsData.map((tag) => { - if (tag.id < 4) { - tag.group = '这是第一组' - } else { - tag.group = '这是第二组' // '__default__' - } - return tag - }), - 'group', - ) - // console.log('tagsByGroup: ', tagsByGroup) - const groupsKeys = keys(tagsByGroup) + const { groupedTags, tagsData, activeTagData } = store + const groupsKeys = keys(groupedTags) return ( - {activeTagData.title && } + {activeTagData.title && ( + { + onTagSelect(tag) + onSelect(tag) + }} + /> + )} {groupsKeys.map((groupKey) => ( { + onTagSelect(tag) + onSelect(tag) + }} /> ))} diff --git a/src/containers/unit/TagsBar/store.js b/src/containers/unit/TagsBar/store.js index 55f479a45..0d8e952cf 100755 --- a/src/containers/unit/TagsBar/store.js +++ b/src/containers/unit/TagsBar/store.js @@ -7,7 +7,7 @@ import { types as T, getParent } from 'mobx-state-tree' import { findIndex, propEq } from 'ramda' import { TOPIC } from '@/constant' -import { markStates, buildLog, stripMobx } from '@/utils' +import { markStates, buildLog, stripMobx, groupByKey } from '@/utils' import { Tag } from '@/model' /* eslint-disable-next-line */ @@ -40,6 +40,19 @@ const TagsBar = T.model('TagsBar', { get activeTagData() { return stripMobx(self.activeTag) || { title: '', color: '' } }, + get groupedTags() { + return groupByKey( + self.tagsData.map((tag) => { + if (tag.id < 4) { + tag.group = '这是第一组' + } else { + tag.group = '这是第二组' // '__default__' + } + return tag + }), + 'group', + ) + }, })) .actions((self) => ({ selectTag(tag) { diff --git a/src/containers/unit/TagsBar/styles/desktop_view/tag_item.js b/src/containers/unit/TagsBar/styles/desktop_view/tag_item.js index 8683333f8..45d3822eb 100644 --- a/src/containers/unit/TagsBar/styles/desktop_view/tag_item.js +++ b/src/containers/unit/TagsBar/styles/desktop_view/tag_item.js @@ -8,7 +8,7 @@ import { TagsWrapper } from './index' export const Wrapper = styled.div` ${css.flex('align-center')}; - margin-bottom: ${({ inline }) => (!inline ? '5px' : 0)}; + margin-bottom: ${({ inline }) => (!inline ? '3px' : 0)}; padding: ${({ inline }) => (!inline ? '5px' : 0)}; max-width: 180px; border-radius: 5px; diff --git a/src/pages/help-center.js b/src/pages/help-center.js new file mode 100755 index 000000000..ee1018eba --- /dev/null +++ b/src/pages/help-center.js @@ -0,0 +1,124 @@ +import React from 'react' +import { Provider } from 'mobx-react' +import { merge } from 'ramda' + +import { SITE_URL } from '@/config' +import { ROUTE, METRIC } from '@/constant' + +import { + getJwtToken, + makeGQClient, + ssrAmbulance, + parseTheme, + akaTranslate, + nilOrEmpty, + ssrParseURL, +} from '@/utils' +import { P } from '@/schemas' + +import GlobalLayout from '@/containers/layout/GlobalLayout' +import HelpCenterContent from '@/containers/content/HelpCenterContent' + +import { useStore } from '@/stores/init' + +const fetchData = async (props, opt) => { + const { realname } = merge({ realname: true }, opt) + + const token = realname ? getJwtToken(props) : null + const gqClient = makeGQClient(token) + const userHasLogin = nilOrEmpty(token) === false + + // const { asPath } = props + // schema + + const { communityPath } = ssrParseURL(props.req) + const community = akaTranslate(communityPath) + + // query data + const sessionState = gqClient.request(P.sessionState) + const curCommunity = gqClient.request(P.community, { + raw: community, + userHasLogin, + }) + + return { + ...(await sessionState), + ...(await curCommunity), + } +} + +export const getServerSideProps = async (props) => { + const { communityPath } = ssrParseURL(props.req) + + let resp + try { + resp = await fetchData(props) + } catch (e) { + const { + response: { errors }, + } = e + console.log('get errors: ', errors) + if (ssrAmbulance.hasLoginError(errors)) { + resp = await fetchData(props, { realname: false }) + } else { + return { + props: { + errorCode: 404, + target: communityPath, + viewing: { + community: { + raw: communityPath, + title: communityPath, + desc: communityPath, + }, + }, + }, + } + } + } + + const { sessionState, community } = resp + + // // init state on server side + const initProps = merge( + { + theme: { + curTheme: parseTheme(sessionState), + }, + account: { + user: sessionState.user || {}, + isValidSession: sessionState.isValid, + }, + route: { + communityPath: community.raw, + mainPath: community.raw, + }, + viewing: { + community, + }, + }, + {}, + ) + + return { props: { errorCode: null, ...initProps } } +} + +const HelpCenterPage = (props) => { + const store = useStore(props) + + const seoConfig = { + url: `${SITE_URL}/${ROUTE.HELP_CENTER}`, + title: '帮助中心 | xxx', + description: 'xxx help-center', + } + + return ( + + + + + + ) +} + +export default HelpCenterPage diff --git a/src/stores/RootStore/index.js b/src/stores/RootStore/index.js index cc1a62ab5..d0b7ed856 100755 --- a/src/stores/RootStore/index.js +++ b/src/stores/RootStore/index.js @@ -94,6 +94,7 @@ import { CoolGuideContentStore, // GEN: IMPORT SUBSTORE + HelpCenterContentStore, CommunityJoinBadgeStore, ArticleEditorStore, WorksEditorStore, @@ -211,6 +212,7 @@ const rootStore = T.model({ coolGuideContent: T.optional(CoolGuideContentStore, {}), // GEN: PLUG SUBSTORE TO ROOTSTORE + helpCenterContent: T.optional(HelpCenterContentStore, {}), communityJoinBadge: T.optional(CommunityJoinBadgeStore, {}), articleEditor: T.optional(ArticleEditorStore, {}), worksEditor: T.optional(WorksEditorStore, {}), diff --git a/src/stores/index.js b/src/stores/index.js index 353a9af91..474fab567 100755 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -96,6 +96,7 @@ export { default as CommunityEditorStore } from '@/containers/editor/CommunityEd export { default as WorksEditorStore } from '@/containers/editor/WorksEditor/store' // GEN: EXPORT CONTAINERS STORE HERE +export { default as HelpCenterContentStore } from '@/containers/content/HelpCenterContent/store' export { default as CommunityJoinBadgeStore } from '@/containers/tool/CommunityJoinBadge/store' export { default as ArticleEditorStore } from '@/containers/editor/ArticleEditor/store' export { default as UserProfileStore } from '@/containers/user/UserProfile/store' diff --git a/src/stores/init.js b/src/stores/init.js old mode 100644 new mode 100755 diff --git a/utils/constant/metric.js b/utils/constant/metric.js index 1c2fb0546..cdb0deaa1 100644 --- a/utils/constant/metric.js +++ b/utils/constant/metric.js @@ -1,4 +1,4 @@ -// NOTE: the value is mapping to @/utils/width's key +// NOTE: the value is mapping to @/utils/media's key // so do not change to lowercase etc... const METRIC = { COMMUNITY: 'COMMUNITY', @@ -22,6 +22,8 @@ const METRIC = { WORKS_EDITOR: 'WORKS_EDITOR', COMMUNITY_EDITOR: 'COMMUNITY_EDITOR', ARTICLE_EDITOR: 'ARTICLE_EDITOR', + + HELP_CENTER: 'HELP_CENTER', } export default METRIC diff --git a/utils/constant/route.js b/utils/constant/route.js index d9e6178c4..0b004d714 100755 --- a/utils/constant/route.js +++ b/utils/constant/route.js @@ -32,6 +32,8 @@ const ROUTE = { RECIPES: 'recipes', SUBSCRIBE: 'subscribe', MEMBERSHIP: 'membership', + + HELP_CENTER: 'help-center', } export default ROUTE diff --git a/utils/css/media.js b/utils/css/media.js index 87a57711c..05d56503e 100644 --- a/utils/css/media.js +++ b/utils/css/media.js @@ -80,6 +80,12 @@ export const WIDTH = { PAGE: '1461px', CONTENT: '650px', }, + + HELP_CENTER: { + PAGE: '1460px', + CONTENT: '1024px', + CONTENT_OFFSET: '10px', + }, } // get page content max width