diff --git a/.eslintrc.js b/.eslintrc.js index 88691e86a..6e4de6ad0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -131,11 +131,12 @@ module.exports = { }, }, { - files: ['*.spec.tsx'], + files: ['*.spec.tsx', '*.test.tsx', '*.stories.tsx'], rules: { '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }, ], diff --git a/jest.config.js b/jest.config.js index f28fd91b4..bb5adf2ca 100644 --- a/jest.config.js +++ b/jest.config.js @@ -177,12 +177,9 @@ module.exports = { // transform: undefined, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "/node_modules/", - // "\\.pnp\\.[^\\/]+$" - // ], - - transformIgnorePatterns: ['/node_modules/.pnpm/(?!(openapi-typescript-fetch)@)'], + transformIgnorePatterns: [ + '/node_modules/.pnpm/(?!(lodash-es|openapi-typescript-fetch)@)', + ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/packages/client/common.ts b/packages/client/common.ts index 7a196190c..1beb1f3c0 100644 --- a/packages/client/common.ts +++ b/packages/client/common.ts @@ -1,5 +1,6 @@ import type { components } from './types'; +export type User = components['schemas']['User']; export type Avatar = components['schemas']['Avatar']; export type Topic = components['schemas']['Topic']; export type Pagination = Omit; diff --git a/packages/client/group.ts b/packages/client/group.ts index 164582532..f85353f6f 100644 --- a/packages/client/group.ts +++ b/packages/client/group.ts @@ -5,3 +5,4 @@ export * from './common'; export type Group = components['schemas']['Group']; export type GroupProfile = components['schemas']['GroupProfile']; export type GroupMember = components['schemas']['GroupMember']; +export type GroupTopics = components['schemas']['PrivateTopicDetail']; diff --git a/packages/client/topic.ts b/packages/client/topic.ts new file mode 100644 index 000000000..fa0ff0cee --- /dev/null +++ b/packages/client/topic.ts @@ -0,0 +1,16 @@ +import type { components } from './types'; + +export * from './common'; + +export type Comment = components['schemas']['Comment']; +export type Reply = components['schemas']['Comment']['replies'][0]; + +// https://github.com/drwpow/openapi-typescript/issues/941 +export enum State { + Normal = 0, + Closed = 1, + Reopen = 2, + Silent = 5, + DeletedByUser = 6, + DeletedByAdmin = 7, +} diff --git a/packages/client/user.ts b/packages/client/user.ts index 23e1fe26d..5a08be5ef 100644 --- a/packages/client/user.ts +++ b/packages/client/user.ts @@ -1,9 +1,6 @@ -import type { components } from './types'; - export * from './common'; -export type User = components['schemas']['User']; - +// https://github.com/drwpow/openapi-typescript/issues/941 export enum UserGroup { Admin = 1, BangumiAdmin = 2, diff --git a/packages/design/components/Avatar/Avatar.stories.tsx b/packages/design/components/Avatar/Avatar.stories.tsx index 528134097..6cf885725 100644 --- a/packages/design/components/Avatar/Avatar.stories.tsx +++ b/packages/design/components/Avatar/Avatar.stories.tsx @@ -7,7 +7,7 @@ const componentMeta: ComponentMeta = { title: 'modern/Avatar', component: Avatar, args: { - src: 'https://lain.bgm.tv/pic/user/l/000/00/00/1.jpg', + src: 'https://lain.bgm.tv/pic/user/l/icon.jpg', size: 'small', }, }; diff --git a/packages/design/components/Avatar/style/index.less b/packages/design/components/Avatar/style/index.less index c7e51847d..84627727a 100644 --- a/packages/design/components/Avatar/style/index.less +++ b/packages/design/components/Avatar/style/index.less @@ -18,8 +18,8 @@ } &--medium img { - height: 48px; - width: 48px; + height: 60px; + width: 60px; } &--large img { diff --git a/packages/design/components/EditorForm/style/index.less b/packages/design/components/EditorForm/style/index.less index be596816a..e1447b8af 100644 --- a/packages/design/components/EditorForm/style/index.less +++ b/packages/design/components/EditorForm/style/index.less @@ -1,6 +1,7 @@ @import '../../../theme/base'; -@toolbox-with: 215px; +@toolbox-width: 215px; +@text-width: 682px; .bgm-editor { &__container { @@ -10,6 +11,7 @@ border: 2px solid @gray-10; border-radius: 12px; padding: 8px 0 0 10px; + z-index: 9999; } &__toolbox { @@ -17,7 +19,7 @@ justify-content: space-between; align-items: center; height: 26px; - width: @toolbox-with; + width: @toolbox-width; padding: 0 4px; margin-bottom: 10px; @@ -29,13 +31,12 @@ &__text { flex: 1 0 auto; padding: 0; - min-width: @toolbox-with; - width: 682px; + min-width: @toolbox-width; + width: @text-width; border: none; outline: none; color: @gray-100; line-height: 26px; - resize: vertical; &::placeholder { color: @gray-60; @@ -47,6 +48,7 @@ flex-direction: column; align-items: flex-start; justify-content: flex-start; + width: @text-width; } &__submit { diff --git a/packages/design/components/Layout/Layout.stories.tsx b/packages/design/components/Layout/Layout.stories.tsx new file mode 100644 index 000000000..7203f7d32 --- /dev/null +++ b/packages/design/components/Layout/Layout.stories.tsx @@ -0,0 +1,24 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import Layout from '.'; + +const componentMeta: ComponentMeta = { + title: 'Grid/Layout', + component: Layout, +}; + +export default componentMeta; + +const Template: ComponentStory = (args) => ; + +const leftChildren =
; +const rightChildren =
; + +export const Usage = Template.bind({}); + +Usage.args = { + type: 'alpha', + leftChildren, + rightChildren, +}; diff --git a/packages/design/components/Layout/index.tsx b/packages/design/components/Layout/index.tsx new file mode 100644 index 000000000..bae06caf3 --- /dev/null +++ b/packages/design/components/Layout/index.tsx @@ -0,0 +1,22 @@ +import classnames from 'classnames'; +import type { FC } from 'react'; +import React from 'react'; + +export interface LayoutProps { + type: 'alpha' | 'beta'; + className?: string; + leftChildren?: React.ReactNode; + rightChildren?: React.ReactNode; +} + +const Index: FC = ({ leftChildren, rightChildren, type, className }) => { + const containerClassNames = classnames('bgm-layout', `bgm-layout--${type}`, className); + return ( +
+
{leftChildren}
+
{rightChildren}
+
+ ); +}; + +export default Index; diff --git a/packages/design/components/Layout/style/index.less b/packages/design/components/Layout/style/index.less new file mode 100644 index 000000000..34590915f --- /dev/null +++ b/packages/design/components/Layout/style/index.less @@ -0,0 +1,15 @@ +.bgm-layout { + display: flex; + + &--alpha { + .bgm-layout__left { + width: 913px; + flex: 1; + margin-right: 40px; + } + + .bgm-layout__right { + width: 427px; + } + } +} diff --git a/packages/design/components/Layout/style/index.tsx b/packages/design/components/Layout/style/index.tsx new file mode 100644 index 000000000..d74e52ee9 --- /dev/null +++ b/packages/design/components/Layout/style/index.tsx @@ -0,0 +1 @@ +import './index.less'; diff --git a/packages/design/components/Pagination/index.tsx b/packages/design/components/Pagination/index.tsx index f4c583284..f22554b22 100644 --- a/packages/design/components/Pagination/index.tsx +++ b/packages/design/components/Pagination/index.tsx @@ -15,6 +15,9 @@ export interface PaginationProps { total?: number; /* 页码改变的回调 */ onChange?: (offset: number) => void; + + /* 自定义 classname */ + wrapperClass?: string; } function calculatePage(pageSize: number, total: number): number { @@ -25,6 +28,7 @@ const Pagination: FC = ({ currentPage = 1, pageSize = 30, total = 0, + wrapperClass, ...restProps }) => { const [current, setCurrent] = useState(() => currentPage); @@ -108,7 +112,7 @@ const Pagination: FC = ({ } } return ( -
    +
      {prevButton} {pagerList} {nextButton} diff --git a/packages/design/components/RichContent/index.tsx b/packages/design/components/RichContent/index.tsx index 8804e554b..9d5ded7b0 100644 --- a/packages/design/components/RichContent/index.tsx +++ b/packages/design/components/RichContent/index.tsx @@ -1,14 +1,14 @@ import classNames from 'classnames'; import React from 'react'; -export interface RichTextProps { +export interface RichContentProps { html: string; classname?: string; } -const RichContent: React.FC = ({ html, classname }) => { +const RichContent: React.FC = ({ html, classname }) => { return ( -
      diff --git a/packages/design/components/RichContent/style/index.less b/packages/design/components/RichContent/style/index.less index a94014706..e35f755fa 100644 --- a/packages/design/components/RichContent/style/index.less +++ b/packages/design/components/RichContent/style/index.less @@ -9,4 +9,8 @@ color: @blue-100; text-decoration: none; } + + img { + max-width: 99%; + } } diff --git a/packages/design/components/Section/index.tsx b/packages/design/components/Section/index.tsx index 7d14addc7..ed17ae9ad 100644 --- a/packages/design/components/Section/index.tsx +++ b/packages/design/components/Section/index.tsx @@ -1,13 +1,20 @@ +import classnames from 'classnames'; import type { PropsWithChildren } from 'react'; import React from 'react'; export interface SectionProps { title: string; renderFooter?: () => React.ReactNode; + wrapperClass?: string; } -const Section = ({ title, children, renderFooter }: PropsWithChildren) => { +const Section = ({ + title, + wrapperClass, + children, + renderFooter, +}: PropsWithChildren) => { return ( -
      +

      {title}

      {children} {renderFooter &&
      {renderFooter()}
      } diff --git a/packages/design/components/Topic/Comment.stories.tsx b/packages/design/components/Topic/Comment.stories.tsx new file mode 100644 index 000000000..f11aa28c8 --- /dev/null +++ b/packages/design/components/Topic/Comment.stories.tsx @@ -0,0 +1,76 @@ +import type { ComponentStory } from '@storybook/react'; +import React from 'react'; + +import { State } from '@bangumi/client/topic'; + +import repliesComment from './__test__/fixtures/repliesComment.json'; +import singleComment from './__test__/fixtures/singleComment.json'; +import specialComment from './__test__/fixtures/specialComment.json'; +import mockedCurrentUser from './__test__/fixtures/user.json'; +import type { CommentProps } from './Comment'; +import Comment from './Comment'; + +export default { + title: 'Topic/Comment', + component: Comment, +}; + +const Template: ComponentStory = (args: CommentProps & { states?: State[] }) => { + // 0 正常评论 6 被用户删除 7 违反社区指导原则,已被删除 + // 1 关闭 2 重开 5 下沉 + return ( +
      + {(args.states ?? [0]).map((state, idx) => ( +
      +

      State: {state}

      + +
      + ))} +
      + ); +}; + +export const SingleComment = Template.bind({}); + +SingleComment.args = { + ...singleComment, + isReply: false, + is_friend: true, + originalPosterId: 1, + created_at: String(new Date()), + floor: 2, + states: [State.Normal, State.DeletedByUser, State.DeletedByAdmin], +} as any; + +export const CommentWithReplies = Template.bind({}); +CommentWithReplies.args = { + ...repliesComment, + isReply: false, + is_friend: false, + created_at: String(new Date()), + floor: 2, +} as any; + +export const SelfComment = Template.bind({}); + +const selfUser = { ...mockedCurrentUser, id: 1 }; + +SelfComment.args = { + ...singleComment, + isReply: false, + is_friend: false, + created_at: String(new Date()), + user: selfUser, + floor: 2, +} as any; + +export const SpecialComment = Template.bind({}); + +SpecialComment.args = { + ...specialComment, + isReply: false, + is_friend: false, + created_at: String(new Date()), + floor: 2, + states: [State.Closed, State.Reopen, State.Silent], +} as any; diff --git a/packages/design/components/Topic/Comment.tsx b/packages/design/components/Topic/Comment.tsx new file mode 100644 index 000000000..7ad54d14c --- /dev/null +++ b/packages/design/components/Topic/Comment.tsx @@ -0,0 +1,182 @@ +import classNames from 'classnames'; +import { unescape } from 'lodash-es'; +import type { FC } from 'react'; +import React, { useState } from 'react'; + +import { State } from '@bangumi/client/topic'; +import type { Reply, Comment as IComment, User } from '@bangumi/client/topic'; +import { Friend, OriginalPoster, TopicClosed, TopicSilent, TopicReopen } from '@bangumi/icons'; +import { render as renderBBCode } from '@bangumi/utils'; +import { getUserProfileLink } from '@bangumi/utils/pages'; + +import Avatar from '../../components/Avatar'; +import Button from '../../components/Button'; +import EditorForm from '../../components/EditorForm'; +import RichContent from '../../components/RichContent'; +import Typography from '../../components/Typography'; +import CommentInfo from './CommentInfo'; + +export type CommentProps = ((Reply & { isReply: true }) | (IComment & { isReply: false })) & { + floor: string | number; + originalPosterId: number; + user?: User; +}; + +const Link = Typography.Link; + +const RenderContent: FC<{ state: State; text: string }> = ({ state, text }) => { + switch (state) { + case State.Normal: + return ; + case State.Closed: + return
      关闭了该主题
      ; + case State.Reopen: + return
      重新开启了该主题
      ; + case State.Silent: + return
      下沉了该主题
      ; + case State.DeletedByUser: + return
      内容已被用户删除
      ; + case State.DeletedByAdmin: + return ( +
      + 内容因违反「 + + 社区指导原则 + + 」已被删除 +
      + ); + default: + return null; + } +}; + +const Comment: FC = ({ + text, + creator, + created_at: createAt, + floor, + is_friend: isFriend, + originalPosterId, + state, + user, + ...props +}) => { + const isReply = props.isReply; + const isDeleted = state === State.DeletedByUser || state === State.DeletedByAdmin; + // 1 关闭 2 重开 5 下沉 + const isSpecial = state === State.Closed || state === State.Reopen || state === State.Silent; + const replies = !isReply ? props.replies : null; + const [shouldCollapsed, setShouldCollapsed] = useState( + isSpecial || (isReply && (/[+-]\d+$/.test(text!) || isDeleted)), + ); + const [showReplyEditor, setShowReplyEditor] = useState(false); + + const headerClassName = classNames('bgm-comment__header', { + 'bgm-comment__header--reply': isReply, + 'bgm-comment__header--collapsed': shouldCollapsed, + }); + + const url = getUserProfileLink(creator.username); + + if (shouldCollapsed) { + let icon = null; + switch (state) { + case State.Normal: + icon = null; + break; + case State.Closed: + icon = ; + break; + case State.Reopen: + icon = ; + break; + case State.Silent: + icon = ; + break; + } + + return ( +
      setShouldCollapsed(false)} + id={`post_${props.id}`} + > + +
      + {icon} + + {creator.nickname} + + +
      + +
      +
      + ); + } + + return ( +
      +
      + +
      +
      + +
      + + {creator.nickname} + + {originalPosterId === creator.id ? : null} + {isFriend ? : null} + {!isReply && creator.sign ? {`// ${unescape(creator.sign)}`} : null} +
      + +
      + +
      + {user ? ( +
      + {showReplyEditor ? ( + setShowReplyEditor(false)} + placeholder={`回复给 @${creator.nickname}:`} + /> + ) : ( + <> + + + {user.id === creator.id ? ( + <> + + + + ) : null} + + )} +
      + ) : null} +
      +
      + {replies?.map((reply, idx) => ( + + ))} +
      + ); +}; + +export default Comment; diff --git a/packages/design/components/Topic/CommentInfo.tsx b/packages/design/components/Topic/CommentInfo.tsx new file mode 100644 index 000000000..cb8e6aba6 --- /dev/null +++ b/packages/design/components/Topic/CommentInfo.tsx @@ -0,0 +1,29 @@ +import dayjs from 'dayjs'; +import type { FC } from 'react'; +import React from 'react'; + +export interface CommentInfoProps { + floor: string | number; + isSpecial?: boolean; + createdAt: string | Date; + id?: string; +} + +const spaces = '\u00A0'.repeat(2); + +// Todo: report +const CommentInfo: FC = ({ floor, createdAt, isSpecial = false, id = '' }) => { + const date = dayjs(createdAt).format('YYYY-M-D HH:mm'); + return !isSpecial ? ( + + #{floor} + {spaces}|{spaces} + {date} + {spaces}|{spaces}! + + ) : ( + {date} + ); +}; + +export default CommentInfo; diff --git a/packages/design/components/Topic/__test__/Comment.spec.tsx b/packages/design/components/Topic/__test__/Comment.spec.tsx new file mode 100644 index 000000000..1303e3e9e --- /dev/null +++ b/packages/design/components/Topic/__test__/Comment.spec.tsx @@ -0,0 +1,123 @@ +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; + +import type { CommentProps } from '../Comment'; +import Comment from '../Comment'; +import repliesComment from './fixtures/repliesComment.json'; +import singleComment from './fixtures/singleComment.json'; +import specialComment from './fixtures/specialComment.json'; +import mockedCurrentUser from './fixtures/user.json'; + +// 0 正常评论 6 被用户删除 7 违反社区指导原则,已被删除 +describe('Normal Comment', () => { + function buildProps( + isReply = false, + comment?: any, + floor = '233', + originalPosterId = 233, + user = mockedCurrentUser, + ) { + const reply = repliesComment.replies[0]; + const mockedComment = comment ?? (isReply ? reply : singleComment); + return { + ...mockedComment, + floor, + originalPosterId, + user, + isReply, + } as unknown as CommentProps; + } + + it.each([0, 6, 7])('should render %d', (state) => { + const props = buildProps(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should render with reply', () => { + const props = buildProps(false, repliesComment); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('reply should have reply styles', () => { + const props = buildProps(true); + const { container } = render(); + // should have reply styles + expect(container.getElementsByClassName('bgm-comment__header--reply').length).toBe(1); + + // should not have collapsed styles + expect(container.getElementsByClassName('bgm-comment__header--collapsed').length).toBe(0); + }); + + it('reply end with +1/-1 reply should be collapsed', () => { + const props = buildProps(true); + const { container: container1 } = render(); + expect(container1.getElementsByClassName('bgm-comment__header--collapsed').length).toBe(1); + + const { container: container2 } = render(); + expect(container2.getElementsByClassName('bgm-comment__header--collapsed').length).toBe(1); + + // should not have collapsed styles if is not reply + const { container: container3 } = render(); + expect(container3.getElementsByClassName('bgm-comment__header--collapsed').length).toBe(0); + }); + + it('show icons', () => { + const props = buildProps(false); + const { container: container1 } = render( + , + ); + expect(container1.getElementsByClassName('creator-info')[0]!.childNodes).toHaveLength(3); + + const { container: container2 } = render( + , + ); + expect(container2.getElementsByClassName('creator-info')[0]!.childNodes).toHaveLength(3); + + const { container: container3 } = render(); + expect(container3.getElementsByClassName('creator-info')[0]!.childNodes).toHaveLength(4); + }); + + it('show edit and delete button if current user is comment creator', () => { + const user = { ...mockedCurrentUser, id: 1 }; + const props = buildProps(false, singleComment, '233', 233, user); + const { getByText } = render(); + expect(getByText('编辑')).toBeInTheDocument(); + expect(getByText('删除')).toBeInTheDocument(); + }); + + it('do not show opinions if not login', () => { + const props = buildProps(false, singleComment, '233', 233, null as any); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('click reply button should show editor form', () => { + const props = buildProps(false); + const { getByText, container } = render(); + expect(container.getElementsByClassName('bgm-editor__form').length).toBe(0); + + fireEvent.click(getByText('回复')); + expect(container.getElementsByClassName('bgm-editor__form').length).toBe(1); + + fireEvent.click(getByText('取消')); + expect(container.getElementsByClassName('bgm-editor__form').length).toBe(0); + }); +}); + +// 1 关闭 2 重开 5 下沉 +describe('Special Comment', () => { + function buildProps(state: number) { + return { + ...specialComment, + state, + } as unknown as CommentProps; + } + + it.each([1, 2, 5])('should render state is %d', (state) => { + const props = buildProps(state); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/design/components/Topic/__test__/CommentInfo.spec.tsx b/packages/design/components/Topic/__test__/CommentInfo.spec.tsx new file mode 100644 index 000000000..86ee83c05 --- /dev/null +++ b/packages/design/components/Topic/__test__/CommentInfo.spec.tsx @@ -0,0 +1,18 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import CommentInfo from '../CommentInfo'; + +it('special comment should not render floor', () => { + const createdAt = '2022-09-22T06:03:21Z'; + const { container } = render(); + const el = container.getElementsByClassName('bgm-topic__commentInfo')[0]; + expect(el!.textContent).toBe('2022-9-22 06:03'); +}); + +it('normal comment should render floor', () => { + const createdAt = '2022-09-22T06:03:21Z'; + const { container } = render(); + const el = container.getElementsByClassName('bgm-topic__commentInfo')[0]; + expect(el!.textContent).toBe('#1  |  2022-9-22 06:03  |  !'); +}); diff --git a/packages/design/components/Topic/__test__/__snapshots__/Comment.spec.tsx.snap b/packages/design/components/Topic/__test__/__snapshots__/Comment.spec.tsx.snap new file mode 100644 index 000000000..e95ccdfa5 --- /dev/null +++ b/packages/design/components/Topic/__test__/__snapshots__/Comment.spec.tsx.snap @@ -0,0 +1,577 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Normal Comment do not show opinions if not login 1`] = ` +
      +
      +
      +
      + +
      +
      +
      + +
      + + 用户 + + + // 我是签名 + +
      + + + # + 233 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + +
      +
      + 123456789~~~ +
      +
      +
      +
      +
      +
      +`; + +exports[`Normal Comment should render 0 1`] = ` +
      +
      +
      +
      + +
      +
      +
      + +
      + + 用户 + + + // 我是签名 + +
      + + + # + 233 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + +
      +
      + 123456789~~~ +
      +
      +
      + + +
      +
      +
      +
      +
      +`; + +exports[`Normal Comment should render 6 1`] = ` +
      +
      +
      +
      + +
      +
      +
      + +
      + + 用户 + + + // 我是签名 + +
      + + + # + 233 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + +
      +
      + 内容已被用户删除 +
      +
      +
      + + +
      +
      +
      +
      +
      +`; + +exports[`Normal Comment should render 7 1`] = ` +
      +
      +
      +
      + +
      +
      +
      + +
      + + 用户 + + + // 我是签名 + +
      + + + # + 233 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + +
      +
      + 内容因违反「 + + 社区指导原则 + + 」已被删除 +
      +
      +
      + + +
      +
      +
      +
      +
      +`; + +exports[`Normal Comment should render with reply 1`] = ` +
      +
      +
      +
      + +
      +
      +
      + +
      + + 用户 + + + // 我是签名 + +
      + + + # + 233 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + +
      +
      + 123456789~~~ +
      +
      +
      + + +
      +
      +
      +
      +
      +
      + +
      +
      +
      + + + + + # + 233-1 + +    + | +    + 2022-9-22 06:03 +    + | +    + ! + + +
      + 123456789~~~ +
      +
      +
      + + +
      +
      +
      +
      +
      +
      +`; + +exports[`Special Comment should render state is 1 1`] = ` +
      +
      + +
      +
      + + 用户 + +
      + 关闭了该主题 +
      +
      + + 2022-7-17 04:08 + + +
      +
      +`; + +exports[`Special Comment should render state is 2 1`] = ` +
      +
      + +
      +
      + + 用户 + +
      + 重新开启了该主题 +
      +
      + + 2022-7-17 04:08 + + +
      +
      +`; + +exports[`Special Comment should render state is 5 1`] = ` +
      +
      + +
      +
      + + 用户 + +
      + 下沉了该主题 +
      +
      + + 2022-7-17 04:08 + + +
      +
      +`; diff --git a/packages/design/components/Topic/__test__/fixtures/repliesComment.json b/packages/design/components/Topic/__test__/fixtures/repliesComment.json new file mode 100644 index 000000000..afe22978d --- /dev/null +++ b/packages/design/components/Topic/__test__/fixtures/repliesComment.json @@ -0,0 +1,42 @@ +{ + "created_at": "2022-09-22T06:03:21Z", + "text": "123456789~~~", + "creator": { + "avatar": { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/l/icon.jpg" + }, + "sign": "我是签名", + "url": "https://bgm.tv/user/123456789", + "username": "123456789", + "nickname": "用户", + "id": 1, + "user_group": 11 + }, + "replies": [ + { + "created_at": "2022-09-22T06:03:21Z", + "text": "123456789~~~", + "creator": { + "avatar": { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/m/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/s/icon.jpg" + }, + "sign": "我是签名", + "url": "https://bgm.tv/user/123456789", + "username": "123456789", + "nickname": "用户", + "id": 1, + "user_group": 11 + }, + "id": 2104702, + "is_friend": false, + "state": 0 + } + ], + "id": 2104702, + "is_friend": false, + "state": 0 +} diff --git a/packages/design/components/Topic/__test__/fixtures/singleComment.json b/packages/design/components/Topic/__test__/fixtures/singleComment.json new file mode 100644 index 000000000..a36c10c14 --- /dev/null +++ b/packages/design/components/Topic/__test__/fixtures/singleComment.json @@ -0,0 +1,21 @@ +{ + "created_at": "2022-09-22T06:03:21Z", + "text": "123456789~~~", + "creator": { + "avatar": { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/m/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/s/icon.jpg" + }, + "sign": "我是签名", + "url": "https://bgm.tv/user/123456789", + "username": "123456789", + "nickname": "用户", + "id": 1, + "user_group": 11 + }, + "replies": [], + "id": 2104702, + "is_friend": false, + "state": 0 +} diff --git a/packages/design/components/Topic/__test__/fixtures/specialComment.json b/packages/design/components/Topic/__test__/fixtures/specialComment.json new file mode 100644 index 000000000..d8bf5585a --- /dev/null +++ b/packages/design/components/Topic/__test__/fixtures/specialComment.json @@ -0,0 +1,21 @@ +{ + "created_at": "2022-07-17T04:08:38Z", + "text": "", + "creator": { + "avatar": { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/m/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/s/icon.jpg" + }, + "sign": "我是签名", + "url": "https://bgm.tv/user/123456789", + "username": "123456789", + "nickname": "用户", + "id": 123456789, + "user_group": 0 + }, + "replies": [], + "id": 2056216, + "is_friend": false, + "state": 0 +} diff --git a/packages/design/components/Topic/__test__/fixtures/user.json b/packages/design/components/Topic/__test__/fixtures/user.json new file mode 100644 index 000000000..8f8a466d1 --- /dev/null +++ b/packages/design/components/Topic/__test__/fixtures/user.json @@ -0,0 +1,13 @@ +{ + "avatar": { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/m/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/s/icon.jpg" + }, + "sign": "我是签名", + "url": "https://bgm.tv/user/123456789", + "username": "123456789", + "nickname": "用户", + "id": 10, + "user_group": 11 +} diff --git a/packages/design/components/Topic/index.tsx b/packages/design/components/Topic/index.tsx new file mode 100644 index 000000000..a46d13976 --- /dev/null +++ b/packages/design/components/Topic/index.tsx @@ -0,0 +1,10 @@ +import Comment from './Comment'; +import CommentInfo from './CommentInfo'; + +export default { + Comment, + CommentInfo, +}; + +export type { CommentProps } from './Comment'; +export type { CommentInfoProps } from './CommentInfo'; diff --git a/packages/design/components/Topic/style/Comment.less b/packages/design/components/Topic/style/Comment.less new file mode 100644 index 000000000..edfab752f --- /dev/null +++ b/packages/design/components/Topic/style/Comment.less @@ -0,0 +1,82 @@ +@import '../../../theme/base'; + +.bgm-comment { + &__header { + display: flex; + align-items: flex-start; + max-width: 913px; + padding-top: 20px; + padding-bottom: 12px; + border-bottom: 1px dashed @gray-10; + + &--reply { + margin-left: 72px; + } + + &--collapsed { + padding-top: 12px; + min-height: 22px; + } + } + + &__box { + margin-left: 12px; + width: 100%; + } + + &__main { + min-height: 60px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + width: 100%; + } + + &__tip { + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: space-between; + color: @gray-60; + width: 100%; + + .creator-info { + display: flex; + align-items: center; + justify-content: space-between; + } + + div > a, + svg { + padding-right: 12px; + } + } + + &__opinions { + margin-top: 12px; + + > .bgm-button { + height: 24px; + line-height: 22px; + margin-right: 10px; + } + + > .bgm-button__secondary { + background-color: #fff; + border: 2px solid @gray-10; + border-radius: 12px; + padding: 0 15px; + } + } + + &__content { + color: @gray-100; + line-height: 26px; + + &--deleted { + color: @gray-80; + } + } +} diff --git a/packages/design/components/Topic/style/CommentInfo.less b/packages/design/components/Topic/style/CommentInfo.less new file mode 100644 index 000000000..50da60fec --- /dev/null +++ b/packages/design/components/Topic/style/CommentInfo.less @@ -0,0 +1,12 @@ +@import '../../../theme/base'; + +.bgm-topic__commentInfo { + color: @gray-60; + font-size: 14px; + line-height: 20px; + + a { + color: @gray-60; + text-decoration: none; + } +} diff --git a/packages/design/components/Topic/style/index.tsx b/packages/design/components/Topic/style/index.tsx new file mode 100644 index 000000000..6891be189 --- /dev/null +++ b/packages/design/components/Topic/style/index.tsx @@ -0,0 +1,2 @@ +import './Comment.less'; +import './CommentInfo.less'; diff --git a/packages/design/index.tsx b/packages/design/index.tsx index 43eeca513..ebe01c8e3 100644 --- a/packages/design/index.tsx +++ b/packages/design/index.tsx @@ -9,7 +9,10 @@ export { default as Avatar } from './components/Avatar'; export { default as Input } from './components/Input'; export { default as Section } from './components/Section'; export { default as EditorForm } from './components/EditorForm'; +export { default as RichContent } from './components/RichContent'; export { default as Pagination } from './components/Pagination'; +export { default as Topic } from './components/Topic'; +export { default as Layout } from './components/Layout'; export type { ButtonProps } from './components/Button'; export type { LinkProps } from './components/Typography'; @@ -22,4 +25,6 @@ export type { AvatarProps } from './components/Avatar'; export type { InputProps } from './components/Input'; export type { SectionProps } from './components/Section'; export type { EditorFormProps } from './components/EditorForm'; +export type { RichContentProps } from './components/RichContent'; export type { PaginationProps } from './components/Pagination'; +export type { LayoutProps } from './components/Layout'; diff --git a/packages/design/package.json b/packages/design/package.json index a02092fa6..429b4e17f 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -4,18 +4,22 @@ "private": true, "main": "index.tsx", "dependencies": { + "dayjs": "^1.11.3", + "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "reset-css": "^5.0.1" }, "devDependencies": { "@bangumi/icons": "workspace:*", + "@bangumi/client": "workspace:*", "@bangumi/utils": "workspace:*", "@storybook/addon-essentials": "^6.5.13", "@storybook/addon-links": "^6.5.13", "@storybook/builder-vite": "^0.2.5", "@storybook/react": "^6.5.13", "@testing-library/react": "^13.4.0", + "@types/lodash-es": "^4.17.6", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "classnames": "^2.3.2", diff --git a/packages/icons/assets/friend.svg b/packages/icons/assets/friend.svg new file mode 100644 index 000000000..ade2bc437 --- /dev/null +++ b/packages/icons/assets/friend.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/assets/original-poster.svg b/packages/icons/assets/original-poster.svg new file mode 100644 index 000000000..36bcb9d90 --- /dev/null +++ b/packages/icons/assets/original-poster.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/assets/topic-closed.svg b/packages/icons/assets/topic-closed.svg new file mode 100644 index 000000000..31c7551eb --- /dev/null +++ b/packages/icons/assets/topic-closed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/icons/assets/topic-reopen.svg b/packages/icons/assets/topic-reopen.svg new file mode 100644 index 000000000..fdaa23f5b --- /dev/null +++ b/packages/icons/assets/topic-reopen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/icons/assets/topic-silent.svg b/packages/icons/assets/topic-silent.svg new file mode 100644 index 000000000..289ebd599 --- /dev/null +++ b/packages/icons/assets/topic-silent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/icons/index.tsx b/packages/icons/index.tsx index 19684e05a..20b3900fc 100644 --- a/packages/icons/index.tsx +++ b/packages/icons/index.tsx @@ -20,3 +20,11 @@ export { ReactComponent as Underscore } from './assets/underscore.svg'; export { ReactComponent as Image } from './assets/image.svg'; export { ReactComponent as Link } from './assets/link.svg'; export { ReactComponent as Size } from './assets/size.svg'; + +// Topic + +export { ReactComponent as OriginalPoster } from './assets/original-poster.svg'; +export { ReactComponent as Friend } from './assets/friend.svg'; +export { ReactComponent as TopicSilent } from './assets/topic-silent.svg'; +export { ReactComponent as TopicClosed } from './assets/topic-closed.svg'; +export { ReactComponent as TopicReopen } from './assets/topic-reopen.svg'; diff --git a/packages/website/src/utils/pages.ts b/packages/utils/pages.ts similarity index 100% rename from packages/website/src/utils/pages.ts rename to packages/utils/pages.ts diff --git a/packages/website/src/hooks/use-group-topic.ts b/packages/website/src/hooks/use-group-topic.ts new file mode 100644 index 000000000..235bc9ac5 --- /dev/null +++ b/packages/website/src/hooks/use-group-topic.ts @@ -0,0 +1,20 @@ +import useSWR from 'swr'; + +import { api } from '@bangumi/client'; +import type { GroupTopics, Group } from '@bangumi/client/group'; +type TopicsResp = { + group: Group; +} & GroupTopics; + +function useGroupTopic(id: number): TopicsResp { + const { data: topicDetail } = useSWR( + api.getGroupTopicById.swrKey({ topicID: id }), + api.getGroupTopicById.fetcher, + { + suspense: true, + }, + ); + return topicDetail! as TopicsResp; +} + +export default useGroupTopic; diff --git a/packages/website/src/hooks/use-group.ts b/packages/website/src/hooks/use-group.ts index 0edd4df0d..11bfeb999 100644 --- a/packages/website/src/hooks/use-group.ts +++ b/packages/website/src/hooks/use-group.ts @@ -15,7 +15,10 @@ export interface UseGroupRet { setDescriptionClamp: (val: DescriptionClamp) => void; } -export function useGroupTopic(name: string, { limit = 20, offset = 0 }: Partial = {}) { +export function useGroupRecentTopics( + name: string, + { limit = 20, offset = 0 }: Partial = {}, +) { const { data: recentTopicsResp } = useSWR( api.getGroupTopicsByGroupName.swrKey({ name }, { limit, offset }), api.getGroupTopicsByGroupName.fetcher, diff --git a/packages/website/src/pages/components/UserHome.tsx b/packages/website/src/pages/components/UserHome.tsx index e2a1fd600..4fbb4502d 100644 --- a/packages/website/src/pages/components/UserHome.tsx +++ b/packages/website/src/pages/components/UserHome.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Typography } from '@bangumi/design'; +import { getUserProfileLink } from '@bangumi/utils/pages'; import { useUser } from '../../hooks/use-user'; -import { getUserProfileLink } from '../../utils/pages'; import styles from './UserHome.module.less'; const { Link } = Typography; diff --git a/packages/website/src/pages/index/group/[name]/components/TopicsTable.tsx b/packages/website/src/pages/index/group/[name]/components/TopicsTable.tsx index e362a1062..27414cb10 100644 --- a/packages/website/src/pages/index/group/[name]/components/TopicsTable.tsx +++ b/packages/website/src/pages/index/group/[name]/components/TopicsTable.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type { Topic } from '@bangumi/client/common'; import { Typography } from '@bangumi/design'; -import { getGroupTopicLink, getUserProfileLink } from '@bangumi/website/utils/pages'; +import { getUserProfileLink } from '@bangumi/utils/pages'; import styles from './TopicsTable.module.less'; @@ -24,7 +24,7 @@ const TopicsTable: React.FC<{ topics: Topic[] }> = ({ topics }) => { {/* TODO: replace to Link */} - + {topic.title} diff --git a/packages/website/src/pages/index/group/[name]/forum.tsx b/packages/website/src/pages/index/group/[name]/forum.tsx index 5aedfd527..2bdf8c30c 100644 --- a/packages/website/src/pages/index/group/[name]/forum.tsx +++ b/packages/website/src/pages/index/group/[name]/forum.tsx @@ -2,18 +2,19 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { Pagination } from '@bangumi/design'; -import { useGroupTopic } from '@bangumi/website/hooks/use-group'; +import { useGroupRecentTopics } from '@bangumi/website/hooks/use-group'; import { useTransitionNavigate } from '@bangumi/website/hooks/use-navigate'; import { usePaginationParams } from '@bangumi/website/hooks/use-pagination'; import TopicsTable from './components/TopicsTable'; +import styles from './style.module.less'; const GroupForum = () => { const { name } = useParams(); const [, navigate] = useTransitionNavigate(); const { curPage, offset, pageSize } = usePaginationParams(); - const topics = useGroupTopic(name!, { + const topics = useGroupRecentTopics(name!, { offset, limit: pageSize, }); @@ -30,6 +31,7 @@ const GroupForum = () => { total={topics.total} pageSize={pageSize} currentPage={curPage} + wrapperClass={styles.pagination} onChange={handlePageChange} /> diff --git a/packages/website/src/pages/index/group/[name]/index.tsx b/packages/website/src/pages/index/group/[name]/index.tsx index b508ba531..7b387a7b2 100644 --- a/packages/website/src/pages/index/group/[name]/index.tsx +++ b/packages/website/src/pages/index/group/[name]/index.tsx @@ -4,14 +4,13 @@ import { useParams, Link as RouterLink } from 'react-router-dom'; import { Section } from '@bangumi/design'; import { render as renderBBCode, UnreadableCodeError } from '@bangumi/utils'; import { ReactComponent as RightArrow } from '@bangumi/website/assets/right-arrow.svg'; -import { DescriptionClamp, useGroupTopic } from '@bangumi/website/hooks/use-group'; +import { DescriptionClamp, useGroupRecentTopics } from '@bangumi/website/hooks/use-group'; import { useGroupContext } from '../[name]'; import CommonStyles from '../common.module.less'; -import { ClampableContent } from './components/ClampableContent'; +import { ClampableContent } from '../components/ClampableContent'; import TopicsTable from './components/TopicsTable'; - -const CLAMP_HEIGHT_THRESHOLD = 193; +import styles from './style.module.less'; const GroupHome: React.FC = () => { const { name } = useParams(); @@ -19,7 +18,7 @@ const GroupHome: React.FC = () => { throw new UnreadableCodeError('BUG: name is undefined'); } const groupContext = useGroupContext(); - const recentTopics = useGroupTopic(name); + const recentTopics = useGroupRecentTopics(name); if (!groupContext?.groupRet?.group || !recentTopics.data.length) { return null; @@ -39,13 +38,14 @@ const GroupHome: React.FC = () => { return ( <>
      ( 更多组内讨论 diff --git a/packages/website/src/pages/index/group/[name]/members.module.less b/packages/website/src/pages/index/group/[name]/members.module.less deleted file mode 100644 index cda25c679..000000000 --- a/packages/website/src/pages/index/group/[name]/members.module.less +++ /dev/null @@ -1,25 +0,0 @@ -@import '@bangumi/design/theme/base'; - -.pageContainer { - > * { - margin-bottom: 10px; - } -} - -.columnContainer { - display: flex; -} - -.leftCol { - width: 913px; - flex: 1; - margin-right: 40px; -} - -.members { - margin-bottom: 20px; - display: grid; - grid-template-columns: repeat(3, 290px); - column-gap: 14px; - row-gap: 20px; -} diff --git a/packages/website/src/pages/index/group/[name]/members.tsx b/packages/website/src/pages/index/group/[name]/members.tsx index 514a98a59..3c1ce52b4 100644 --- a/packages/website/src/pages/index/group/[name]/members.tsx +++ b/packages/website/src/pages/index/group/[name]/members.tsx @@ -9,7 +9,7 @@ import { useTransitionNavigate } from '@bangumi/website/hooks/use-navigate'; import { usePaginationParams } from '@bangumi/website/hooks/use-pagination'; import { UserCard } from '../components/UserCard'; -import styles from './members.module.less'; +import styles from './style.module.less'; const GroupMembersPage = () => { const { curPage, offset, pageSize } = usePaginationParams(30); @@ -65,7 +65,12 @@ const GroupMembersPage = () => { ); })}
      - + ); diff --git a/packages/website/src/pages/index/group/[name]/style.module.less b/packages/website/src/pages/index/group/[name]/style.module.less new file mode 100644 index 000000000..1598e3a40 --- /dev/null +++ b/packages/website/src/pages/index/group/[name]/style.module.less @@ -0,0 +1,15 @@ +.recentTopics { + margin-top: 40px; +} + +.pagination { + margin-top: 20px; +} + +.members { + margin-bottom: 20px; + display: grid; + grid-template-columns: repeat(3, 290px); + column-gap: 14px; + row-gap: 20px; +} diff --git a/packages/website/src/pages/index/group/[name]/components/ClampableContent.module.less b/packages/website/src/pages/index/group/components/ClampableContent.module.less similarity index 96% rename from packages/website/src/pages/index/group/[name]/components/ClampableContent.module.less rename to packages/website/src/pages/index/group/components/ClampableContent.module.less index a5fdf9904..47c6fe3a4 100644 --- a/packages/website/src/pages/index/group/[name]/components/ClampableContent.module.less +++ b/packages/website/src/pages/index/group/components/ClampableContent.module.less @@ -4,9 +4,7 @@ > * { margin-bottom: 6px; } -} -.content { width: 100%; color: @gray-100; font-size: 16px; diff --git a/packages/website/src/pages/index/group/[name]/components/ClampableContent.tsx b/packages/website/src/pages/index/group/components/ClampableContent.tsx similarity index 77% rename from packages/website/src/pages/index/group/[name]/components/ClampableContent.tsx rename to packages/website/src/pages/index/group/components/ClampableContent.tsx index c4b2e29e2..67ce91587 100644 --- a/packages/website/src/pages/index/group/[name]/components/ClampableContent.tsx +++ b/packages/website/src/pages/index/group/components/ClampableContent.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import React from 'react'; import { ReactComponent as DownArrow } from '@bangumi/website/assets/down-arrow.svg'; @@ -9,7 +10,8 @@ export interface ClampableContentProps { threshold: number; content: string; isClamped: boolean; - onChange: (isClamped: boolean) => void; + containerClassName?: string; + onChange?: (isClamped: boolean) => void; } export const ClampableContent: React.FC = ({ @@ -17,6 +19,7 @@ export const ClampableContent: React.FC = ({ threshold, isClamped, onChange, + containerClassName, }) => { const [isClampEnabled, setIsClampedEnable] = React.useState(false); const contentRef = React.useRef(null); @@ -34,11 +37,11 @@ export const ClampableContent: React.FC = ({ }; const handleUnclamp = (): void => { - onChange(false); + onChange?.(false); }; const handleClamp = (): void => { - onChange(true); + onChange?.(true); }; const renderControl = (): React.ReactElement | null => { @@ -50,10 +53,12 @@ export const ClampableContent: React.FC = ({ return ( <>
      ...
      -
      - 展开 - -
      + {onChange ? ( +
      + 展开 + +
      + ) : null} ); } @@ -66,10 +71,9 @@ export const ClampableContent: React.FC = ({ }; return ( -
      +
      diff --git a/packages/website/src/pages/index/group/components/GroupLayout.module.less b/packages/website/src/pages/index/group/components/GroupLayout.module.less index 8c6db3710..a6b96074c 100644 --- a/packages/website/src/pages/index/group/components/GroupLayout.module.less +++ b/packages/website/src/pages/index/group/components/GroupLayout.module.less @@ -6,31 +6,6 @@ } } -.columnContainer { - display: flex; -} - -.leftCol, -.rightCol { - > * { - margin-bottom: 20px; - } - - &:last-child { - margin-bottom: 0; - } -} - -.leftCol { - width: 913px; - flex: 1; - margin-right: 40px; -} - -.rightCol { - width: 427px; -} - .newMembers { display: grid; grid-template-columns: repeat(5, 1fr); diff --git a/packages/website/src/pages/index/group/components/GroupLayout.tsx b/packages/website/src/pages/index/group/components/GroupLayout.tsx index cbf1c3778..938e501cb 100644 --- a/packages/website/src/pages/index/group/components/GroupLayout.tsx +++ b/packages/website/src/pages/index/group/components/GroupLayout.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import type { GroupProfile } from '@bangumi/client/group'; -import { Section, Tab } from '@bangumi/design'; +import { Section, Tab, Layout } from '@bangumi/design'; import { keyBy } from '@bangumi/utils'; import { ReactComponent as RightArrow } from '@bangumi/website/assets/right-arrow.svg'; import { useTransitionNavigate } from '@bangumi/website/hooks/use-navigate'; @@ -55,9 +55,10 @@ const GroupLayout: React.FC = ({ group, children, curTab, gro activeKey={curTab} onChange={(_, value) => navigate(value.to(groupName))} /> -
      -
      {children}
      -
      + @@ -86,8 +87,8 @@ const GroupLayout: React.FC = ({ group, children, curTab, gro })}
      -
      -
      + } + />
      ); }; diff --git a/packages/website/src/pages/index/group/components/UserCard.tsx b/packages/website/src/pages/index/group/components/UserCard.tsx index 32e985199..61914a1a9 100644 --- a/packages/website/src/pages/index/group/components/UserCard.tsx +++ b/packages/website/src/pages/index/group/components/UserCard.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Image, Typography } from '@bangumi/design'; -import { getUserProfileLink } from '@bangumi/website/utils/pages'; +import { getUserProfileLink } from '@bangumi/utils/pages'; import styles from './UserCard.module.less'; diff --git a/packages/website/src/pages/index/group/topic/[id].tsx b/packages/website/src/pages/index/group/topic/[id].tsx new file mode 100644 index 000000000..03b279835 --- /dev/null +++ b/packages/website/src/pages/index/group/topic/[id].tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +import ErrorBoundary from '@bangumi/website/components/ErrorBoundary'; + +const GroupTopicPage = () => ( + Topic Not found}> + + +); + +export default GroupTopicPage; diff --git a/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.module.less b/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.module.less new file mode 100644 index 000000000..7c5336cef --- /dev/null +++ b/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.module.less @@ -0,0 +1,38 @@ +@import '@bangumi/design/theme/base'; + +.groupTopicHeader { + display: flex; + align-items: center; + margin-bottom: 20px; + max-width: 913px; +} + +.headerMain { + margin-left: 12px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + width: 100%; +} + +.navBar { + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: space-between; + color: @gray-60; + width: 100%; + + div > a, + div > span { + padding-right: 10px; + } +} + +.title { + font-size: 24px; + line-height: 34px; + color: @gray-100; +} diff --git a/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.tsx b/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.tsx new file mode 100644 index 000000000..7e3fde740 --- /dev/null +++ b/packages/website/src/pages/index/group/topic/[id]/components/GroupTopicHeader.tsx @@ -0,0 +1,44 @@ +import type { FC } from 'react'; +import React from 'react'; + +import type { User, Group } from '@bangumi/client/group'; +import { Avatar, Typography, Topic } from '@bangumi/design'; +import { getUserProfileLink } from '@bangumi/utils/pages'; + +import styles from './GroupTopicHeader.module.less'; + +interface Header { + title: string; + createdAt: string | Date; + creator: User; + group: Group; + id: number; +} + +const Link = Typography.Link; +const CommentInfo = Topic.CommentInfo; + +const GroupTopicHeader: FC
      = ({ title, createdAt, creator, group, id }) => { + return ( +
      + +
      + +
      + + {creator.nickname} + + 发表于 + {group.title} + » + 组内讨论 +
      + +
      +

      {title}

      +
      +
      + ); +}; + +export default GroupTopicHeader; diff --git a/packages/website/src/pages/index/group/topic/[id]/index.module.less b/packages/website/src/pages/index/group/topic/[id]/index.module.less new file mode 100644 index 000000000..8216fab60 --- /dev/null +++ b/packages/website/src/pages/index/group/topic/[id]/index.module.less @@ -0,0 +1,76 @@ +/* stylelint-disable value-no-vendor-prefix */ + +@import '@bangumi/design/theme/base'; + +.replies { + margin-top: 40px; +} + +.replyFormContainer { + display: flex; + align-items: flex-start; + margin-top: 20px; + + .replyForm { + margin-left: 12px; + } +} + +// Right Col +.groupInfo { + display: flex; +} + +.groupDetails { + margin-left: 10px; + + > a { + display: block; + margin-bottom: 6px; + font-weight: 600; + font-size: 16px; + line-height: 22px; + } + + > span { + font-size: 12px; + line-height: 17px; + color: @gray-60; + } +} + +.groupDescription { + margin-top: 20px; + font-size: 14px; + line-height: 20px; + color: @gray-80; +} + +.groupOpinions { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 20px; + + > :global(.bgm-button) { + border: 2px solid @gray-10; + border-radius: 15px; + height: 30px; + line-height: 24px; + padding: 0 32px; + + a { + font-weight: 600; + color: @gray-60; + text-decoration: none; + } + + &:hover { + background-color: @gray-10; + + a { + color: @gray-80; + } + } + } +} diff --git a/packages/website/src/pages/index/group/topic/[id]/index.tsx b/packages/website/src/pages/index/group/topic/[id]/index.tsx new file mode 100644 index 000000000..45fc67748 --- /dev/null +++ b/packages/website/src/pages/index/group/topic/[id]/index.tsx @@ -0,0 +1,117 @@ +import type { FC } from 'react'; +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { + EditorForm, + RichContent, + Avatar, + Section, + Typography, + Button, + Topic, + Layout, +} from '@bangumi/design'; +import { render as renderBBCode } from '@bangumi/utils'; +import useGroupTopic from '@bangumi/website/hooks/use-group-topic'; +import { useUser } from '@bangumi/website/hooks/use-user'; + +import { ClampableContent } from '../../components/ClampableContent'; +import GroupTopicHeader from './components/GroupTopicHeader'; +import styles from './index.module.less'; + +const { Link } = Typography; + +const { Comment } = Topic; + +const TopicPage: FC = () => { + const { id } = useParams(); + if (!id || Number.isNaN(Number(id))) { + throw new Error('BUG: topic id is required'); + } + const topicDetail = useGroupTopic(Number(id)); + const { user } = useUser(); + const originalPosterId = topicDetail.creator.id; + const parsedText = renderBBCode(topicDetail.text); + const isClosed = topicDetail.state === 1; + const { group } = topicDetail; + + // Todo: element highlight style https://github.com/bangumi/frontend/pull/113#issuecomment-1328466708 + // https://github.com/bangumi/frontend/pull/113#issuecomment-1322303601 + useEffect(() => { + const anchor = window.location.hash.slice(1); + document.getElementById(anchor)?.scrollIntoView(true); + }, [topicDetail]); + + return ( + <> + + + {/* Topic content */} +
      + +
      + {/* Topic Comments */} +
      + {topicDetail.comments.map((comment, idx) => ( + + ))} + {/* Reply BBCode Editor */} + {!isClosed && user && ( +
      + + +
      + )} +
      + + } + rightChildren={ +
      +
      + +
      + {group.title} + {`${group.total_members} 名成员`} +
      +
      + +
      + + + +
      +
      + } + /> + + ); +}; +export default TopicPage; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a4a795c8..f81477879 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,7 @@ importers: packages/design: specifiers: + '@bangumi/client': workspace:* '@bangumi/icons': workspace:* '@bangumi/utils': workspace:* '@storybook/addon-essentials': ^6.5.13 @@ -119,10 +120,13 @@ importers: '@storybook/builder-vite': ^0.2.5 '@storybook/react': ^6.5.13 '@testing-library/react': ^13.4.0 + '@types/lodash-es': ^4.17.6 '@types/react': ^18.0.25 '@types/react-dom': ^18.0.9 classnames: ^2.3.2 + dayjs: ^1.11.3 less: ^4.1.3 + lodash-es: ^4.17.21 react: ^18.2.0 react-dom: ^18.2.0 react-router-dom: ^6.4.3 @@ -131,10 +135,13 @@ importers: vite: ^3.2.4 vite-plugin-svgr: ^2.2.2 dependencies: + dayjs: 1.11.6 + lodash-es: 4.17.21 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 reset-css: 5.0.1 devDependencies: + '@bangumi/client': link:../client '@bangumi/icons': link:../icons '@bangumi/utils': link:../utils '@storybook/addon-essentials': 6.5.13_lb6du3saekb5anf2gjv3wxj3oq @@ -142,6 +149,7 @@ importers: '@storybook/builder-vite': 0.2.5_sugbzh45vkst6yuqsfexy2pu44 '@storybook/react': 6.5.13_lb6du3saekb5anf2gjv3wxj3oq '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y + '@types/lodash-es': 4.17.6 '@types/react': 18.0.25 '@types/react-dom': 18.0.9 classnames: 2.3.2 @@ -5978,6 +5986,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/lodash-es/4.17.6: + resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} + dependencies: + '@types/lodash': 4.14.190 + dev: true + /@types/lodash/4.14.190: resolution: {integrity: sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw==} dev: true @@ -12787,6 +12801,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash-es/4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.debounce/4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}