diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 1089e320..c7a40c8b 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -708,6 +708,8 @@ ui: logout: Log out admin: Admin review: Review + bookmark: Bookmarks + moderation: Moderation search: placeholder: Search footer: @@ -903,7 +905,7 @@ ui: Views: Viewed Follow: Follow Following: Following - follow_tip: Follow this question to receive notifications. + follow_tip: Follow this question to receive notifications answered: answered closed_in: Closed in show_exist: Show existing question. diff --git a/ui/src/common/sideNavLayout.scss b/ui/src/common/sideNavLayout.scss new file mode 100644 index 00000000..866e4267 --- /dev/null +++ b/ui/src/common/sideNavLayout.scss @@ -0,0 +1,28 @@ +.page-right-side { + flex: none; + width: 30%; +} + +.line { + position: absolute; + top: 0; + right: 12px; + width: 1px; + height: 100%; + background-color: var(--bs-gray-300); + min-height: calc(100vh - 62px - 74px); +} + +// lg +@media screen and (max-width: 1199.9px) { + .page-right-side { + width: 100%; + } +} + +// md +@media screen and (max-width: 991.9px) { + .line { + display: none; + } +} diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index b79d4310..17514a9f 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -63,29 +63,16 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => { onClick={handleLinkClick}> {t('header.nav.profile')} </Dropdown.Item> + <Dropdown.Item + href={`/users/${userInfo.username}/bookmarks`} + onClick={handleLinkClick}> + {t('header.nav.bookmark')} + </Dropdown.Item> <Dropdown.Item href="/users/settings/profile" onClick={handleLinkClick}> {t('header.nav.setting')} </Dropdown.Item> - {userInfo?.role_id === 2 ? ( - <Dropdown.Item href="/admin" onClick={handleLinkClick}> - {t('header.nav.admin')} - </Dropdown.Item> - ) : null} - {redDot?.can_revision ? ( - <Dropdown.Item - href="/review" - className="position-relative" - onClick={handleLinkClick}> - {t('header.nav.review')} - {redDot?.revision > 0 && ( - <span className="position-absolute top-50 translate-middle-y end-0 me-3 p-2 bg-danger border border-light rounded-circle"> - <span className="visually-hidden">New Review</span> - </span> - )} - </Dropdown.Item> - ) : null} <Dropdown.Divider /> <Dropdown.Item onClick={logOut}> {t('header.nav.logout')} diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 585e93c2..0010ff2d 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -52,10 +52,14 @@ &.theme-light { background: linear-gradient(180deg, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.95) 100%); } + + .maxw-400 { + max-width: 400px;; + } } -@media (max-width: 992.9px) { +@media (max-width: 991.9px) { #header { .nav-grow { flex-grow: 1!important; @@ -65,8 +69,8 @@ display: flex!important; } - .w-75 { - width: 100% !important; + .maxw-400 { + max-width: 100%; } } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 86846dae..67706310 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -11,7 +11,6 @@ import { import { useTranslation } from 'react-i18next'; import { useSearchParams, - NavLink, Link, useNavigate, useLocation, @@ -27,6 +26,7 @@ import { brandingStore, loginSettingStore, themeSettingStore, + sideNavStore, } from '@/stores'; import { logout, useQueryNotificationStatus } from '@/services'; @@ -45,6 +45,7 @@ const Header: FC = () => { const siteInfo = siteInfoStore((state) => state.siteInfo); const brandingInfo = brandingStore((state) => state.branding); const loginSetting = loginSettingStore((state) => state.login); + const { updateReiview, updateVisible } = sideNavStore(); const { data: redDot } = useQueryNotificationStatus(); /** * Automatically append `tag` information when creating a question @@ -55,6 +56,13 @@ const Header: FC = () => { askUrl = `${askUrl}?tags=${tagMatch.params.slugName}`; } + useEffect(() => { + updateReiview({ + can_revision: Boolean(redDot?.can_revision), + revision: Number(redDot?.revision), + }); + }, [redDot]); + const handleInput = (val) => { setSearch(val); }; @@ -106,10 +114,13 @@ const Header: FC = () => { aria-controls="navBarContent" className="answer-navBar me-2" id="navBarToggle" + onClick={() => { + updateVisible(); + }} /> <div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap"> - <Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3 p-0"> + <Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-5 p-0"> {brandingInfo.logo ? ( <> <img @@ -159,26 +170,11 @@ const Header: FC = () => { </div> <Navbar.Collapse id="navBarContent" className="me-auto"> - <hr className="hr lg-none mb-2" style={{ marginTop: '12px' }} /> - <Col md={4}> - <Nav> - <NavLink className="nav-link" to="/questions"> - {t('header.nav.question')} - </NavLink> - <NavLink className="nav-link" to="/tags"> - {t('header.nav.tag')} - </NavLink> - <NavLink className="nav-link" to="/users"> - {t('header.nav.user')} - </NavLink> - </Nav> - </Col> - <hr className="hr lg-none mt-2" /> - - <Col lg={4} className="d-flex justify-content-center"> + <hr className="hr lg-none mb-3" style={{ marginTop: '12px' }} /> + <Col lg={8} className="ps-0"> <Form action="/search" - className="w-75 px-0 px-lg-2" + className="w-100 maxw-400" onSubmit={handleSearch}> <FormControl placeholder={t('header.search.placeholder')} diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss new file mode 100644 index 00000000..8e12db8c --- /dev/null +++ b/ui/src/components/SideNav/index.scss @@ -0,0 +1,25 @@ + +#sideNav { + .nav { + max-width: 172px; + } + .nav-link { + color: rgba(0, 0, 0, 0.65); + } + .nav-link:hover { + color: rgba(0, 0, 0); + background-color: var(--bs-gray-100); + } + .nav-link.active { + color: black; + background-color: var(--bs-gray-200); + } +} + +@media screen and (max-width: 991.9px) { + #sideNav { + .nav { + max-width: 100%; + } + } +} diff --git a/ui/src/components/SideNav/index.tsx b/ui/src/components/SideNav/index.tsx new file mode 100644 index 00000000..a16cd1c5 --- /dev/null +++ b/ui/src/components/SideNav/index.tsx @@ -0,0 +1,74 @@ +import { FC } from 'react'; +import { Col, Nav } from 'react-bootstrap'; +import { NavLink, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import classnames from 'classnames'; + +import { loggedUserInfoStore, sideNavStore } from '@/stores'; +import { Icon } from '@/components'; +import './index.scss'; + +const Index: FC = () => { + const { t } = useTranslation(); + const { pathname } = useLocation(); + const { user: userInfo } = loggedUserInfoStore(); + const { visible, can_revision, revision } = sideNavStore(); + return ( + <Col + xl={2} + lg={3} + md={12} + className={classnames( + 'pt-4 position-relative', + visible ? 'fade-in' : 'd-none d-lg-block', + )} + id="sideNav"> + <Nav variant="pills" className="flex-column"> + <NavLink + to="/questions" + className={({ isActive }) => + isActive || pathname === '/' ? 'nav-link active' : 'nav-link' + }> + <Icon name="question-circle-fill" className="me-2" /> + <span>{t('header.nav.question')}</span> + </NavLink> + + <NavLink to="/tags" className="nav-link"> + <Icon name="tags-fill" className="me-2" /> + <span>{t('header.nav.tag')}</span> + </NavLink> + + <NavLink to="/users" className="nav-link"> + <Icon name="people-fill" className="me-2" /> + <span>{t('header.nav.user')}</span> + </NavLink> + + {can_revision || userInfo?.role_id === 2 ? ( + <> + <div className="py-2 px-3 mt-3 fs-14 fw-bold"> + {t('header.nav.moderation')} + </div> + {can_revision && ( + <NavLink to="/review" className="nav-link"> + <span>{t('header.nav.review')}</span> + <span className="float-end"> + {revision > 99 ? '99+' : revision > 0 ? revision : ''} + </span> + </NavLink> + )} + + {userInfo?.role_id === 2 ? ( + <NavLink to="/admin" className="nav-link"> + {t('header.nav.admin')} + </NavLink> + ) : null} + </> + ) : null} + </Nav> + <div className="line" /> + </Col> + ); +}; + +export default Index; diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index ad83636b..dfe906c5 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -40,6 +40,7 @@ import HotQuestions from './HotQuestions'; import HttpErrorContent from './HttpErrorContent'; import CustomSidebar from './CustomSidebar'; import ImgViewer from './ImgViewer'; +import SideNav from './SideNav'; export { Avatar, @@ -86,5 +87,6 @@ export { HttpErrorContent, CustomSidebar, ImgViewer, + SideNav, }; export type { EditorRef, JSONSchema, UISchema }; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 8d580da0..2923999b 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; +import { Row, Col, Form, Button, Card } from 'react-bootstrap'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -320,14 +320,10 @@ const Ask = () => { title: pageTitle, }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={10} md={12}> - <h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3> - </Col> - </Row> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> + <div className="pt-4 mb-5"> + <h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3> + <Row> + <Col className="flex-auto"> <Form noValidate onSubmit={handleSubmit}> {isEdit && ( <Form.Group controlId="revision" className="mb-3"> @@ -491,7 +487,7 @@ const Ask = () => { )} </Form> </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> + <Col className="page-right-side mt-4 mt-xl-0"> <Card> <Card.Header> {t('title', { keyPrefix: 'how_to_format' })} @@ -505,7 +501,7 @@ const Ask = () => { </Card> </Col> </Row> - </Container> + </div> ); }; diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 84871357..d01cb525 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, @@ -198,69 +198,67 @@ const Index = () => { keywords: question?.tags.map((_) => _.slug_name).join(','), }); return ( - <Container className="pt-4 mt-2 mb-5 questionDetailPage"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0"> - {question?.operation?.level && <Alert data={question.operation} />} - {isLoading ? ( - <ContentLoader /> - ) : ( - <Question - data={question} - initPage={initPage} - hasAnswer={answers.count > 0} - isLogged={isLogged} + <Row className="questionDetailPage pt-4 mb-5"> + <Col className="flex-auto"> + {question?.operation?.level && <Alert data={question.operation} />} + {isLoading ? ( + <ContentLoader /> + ) : ( + <Question + data={question} + initPage={initPage} + hasAnswer={answers.count > 0} + isLogged={isLogged} + /> + )} + {!isLoading && answers.count > 0 && ( + <> + <AnswerHead count={answers.count} order={order} /> + {answers?.list?.map((item) => { + return ( + <Answer + aid={aid} + key={item?.id} + data={item} + questionTitle={question?.title || ''} + slugTitle={question?.url_title} + canAccept={isAuthor || isAdmin || isModerator} + callback={initPage} + isLogged={isLogged} + /> + ); + })} + </> + )} + + {!isLoading && Math.ceil(answers.count / 15) > 1 && ( + <div className="d-flex justify-content-center answer-item pt-4"> + <Pagination + currentPage={Number(page || 1)} + pageSize={15} + totalSize={answers?.count || 0} + /> + </div> + )} + + {!isLoading && + Number(question?.status) !== 2 && + !question?.operation?.type && ( + <WriteAnswer + data={{ + qid, + answered: question?.answered, + loggedUserRank, + }} + callback={writeAnswerCallback} /> )} - {!isLoading && answers.count > 0 && ( - <> - <AnswerHead count={answers.count} order={order} /> - {answers?.list?.map((item) => { - return ( - <Answer - aid={aid} - key={item?.id} - data={item} - questionTitle={question?.title || ''} - slugTitle={question?.url_title} - canAccept={isAuthor || isAdmin || isModerator} - callback={initPage} - isLogged={isLogged} - /> - ); - })} - </> - )} - - {!isLoading && Math.ceil(answers.count / 15) > 1 && ( - <div className="d-flex justify-content-center answer-item pt-4"> - <Pagination - currentPage={Number(page || 1)} - pageSize={15} - totalSize={answers?.count || 0} - /> - </div> - )} - - {!isLoading && - Number(question?.status) !== 2 && - !question?.operation?.type && ( - <WriteAnswer - data={{ - qid, - answered: question?.answered, - loggedUserRank, - }} - callback={writeAnswerCallback} - /> - )} - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> - <CustomSidebar /> - <RelatedQuestions id={question?.id || ''} /> - </Col> - </Row> - </Container> + </Col> + <Col className="page-right-side mt-4 mt-xl-0"> + <CustomSidebar /> + <RelatedQuestions id={question?.id || ''} /> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 9875af96..f0eb3f78 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; -import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; +import { Row, Col, Form, Button, Card } from 'react-bootstrap'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -184,14 +184,10 @@ const Index = () => { title: t('edit_answer', { keyPrefix: 'page_title' }), }); return ( - <Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> - <Row className="justify-content-center"> - <Col xxl={10} md={12}> - <h3 className="mb-4">{t('title')}</h3> - </Col> - </Row> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> + <div className="pt-4 mb-5 edit-answer-wrap"> + <h3 className="mb-4">{t('title')}</h3> + <Row> + <Col className="flex-auto"> <a href={pathFactory.questionLanding(qid, data?.question.url_title)} target="_blank" @@ -285,7 +281,7 @@ const Index = () => { </div> </Form> </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> + <Col className="page-right-side mt-4 mt-xl-0"> <Card> <Card.Header> {t('title', { keyPrefix: 'how_to_format' })} @@ -299,7 +295,7 @@ const Index = () => { </Card> </Col> </Row> - </Container> + </div> ); }; diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx index 0d01afd5..556cebaf 100644 --- a/ui/src/pages/Questions/index.tsx +++ b/ui/src/pages/Questions/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useMatch, Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -44,48 +44,46 @@ const Questions: FC = () => { usePageTags({ title: pageTitle, subtitle: slogan }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12}> - <QuestionList - source="questions" - data={listData} - isLoading={listLoading} - /> - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> - <CustomSidebar /> - {!loggedUser.username && ( - <div className="card mb-4"> - <div className="card-body"> - <h5 className="card-title"> - {t2('website_welcome', { - site_name: siteInfo.name, - })} - </h5> - <p className="card-text">{siteInfo.description}</p> + <Row className="pt-4 mb-5"> + <Col className="flex-auto"> + <QuestionList + source="questions" + data={listData} + isLoading={listLoading} + /> + </Col> + <Col className="page-right-side mt-4 mt-xl-0"> + <CustomSidebar /> + {!loggedUser.username && ( + <div className="card mb-4"> + <div className="card-body"> + <h5 className="card-title"> + {t2('website_welcome', { + site_name: siteInfo.name, + })} + </h5> + <p className="card-text">{siteInfo.description}</p> + <Link + to={userCenter.getLoginUrl()} + className="btn btn-primary" + onClick={floppyNavigation.handleRouteLinkClick}> + {t('login', { keyPrefix: 'btns' })} + </Link> + {loginSetting.allow_new_registrations ? ( <Link - to={userCenter.getLoginUrl()} - className="btn btn-primary" + to={userCenter.getSignUpUrl()} + className="btn btn-link ms-2" onClick={floppyNavigation.handleRouteLinkClick}> - {t('login', { keyPrefix: 'btns' })} + {t('signup', { keyPrefix: 'btns' })} </Link> - {loginSetting.allow_new_registrations ? ( - <Link - to={userCenter.getSignUpUrl()} - className="btn btn-link ms-2" - onClick={floppyNavigation.handleRouteLinkClick}> - {t('signup', { keyPrefix: 'btns' })} - </Link> - ) : null} - </div> + ) : null} </div> - )} - {loggedUser.access_token && <FollowingTags />} - <HotQuestions /> - </Col> - </Row> - </Container> + </div> + )} + {loggedUser.access_token && <FollowingTags />} + <HotQuestions /> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Review/index.tsx b/ui/src/pages/Review/index.tsx index c328a823..b6179859 100644 --- a/ui/src/pages/Review/index.tsx +++ b/ui/src/pages/Review/index.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useState } from 'react'; -import { Container, Row, Col, Alert, Stack, Button } from 'react-bootstrap'; +import { Row, Col, Alert, Stack, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -123,121 +123,111 @@ const Index: FC = () => { title: t('review'), }); return ( - <Container className="pt-2 mt-4 mb-5"> - <Row> - <Col lg={{ span: 7, offset: 1 }}> - <h3 className="mb-4">{t('review')}</h3> - </Col> + <Row className="pt-4 mb-5"> + <h3 className="mb-4">{t('review')}</h3> + <Col className="flex-auto"> {!noTasks && ro && ( <> - <Col lg={{ span: 7, offset: 1 }}> - <Alert variant="secondary"> - <Stack className="align-items-start"> - <span className="badge text-bg-secondary mb-2"> - {editBadge} - </span> - <Link to={itemLink} target="_blank"> - {itemTitle} - </Link> - <p className="mb-0"> - {t('edit_summary')}: {editSummary} - </p> - </Stack> - <Stack - direction="horizontal" - gap={1} - className="align-items-baseline mt-2"> - <BaseUserCard data={editor} avatarSize="24" /> - {editTime && ( - <FormatTime - time={editTime} - className="small text-secondary" - preFix={t('proposed')} - /> - )} - </Stack> - </Alert> - </Col> - <Col lg={{ span: 7, offset: 1 }}> - {type === 'question' && - info && - reviewInfo && - 'content' in reviewInfo && ( - <DiffContent - className="mt-2" - objectType={type} - oldData={{ - title: info.title, - original_text: info.content, - tags: info.tags, - }} - newData={{ - title: reviewInfo.title, - original_text: reviewInfo.content, - tags: reviewInfo.tags, - }} + <Alert variant="secondary"> + <Stack className="align-items-start"> + <span className="badge text-bg-secondary mb-2"> + {editBadge} + </span> + <Link to={itemLink} target="_blank"> + {itemTitle} + </Link> + <p className="mb-0"> + {t('edit_summary')}: {editSummary} + </p> + </Stack> + <Stack + direction="horizontal" + gap={1} + className="align-items-baseline mt-2"> + <BaseUserCard data={editor} avatarSize="24" /> + {editTime && ( + <FormatTime + time={editTime} + className="small text-secondary" + preFix={t('proposed')} /> )} - {type === 'answer' && - info && - reviewInfo && - 'content' in reviewInfo && ( - <DiffContent - className="mt-2" - objectType={type} - newData={{ - original_text: reviewInfo.content, - }} - oldData={{ - original_text: info.content, - }} - /> - )} - {type === 'tag' && info && reviewInfo && ( + </Stack> + </Alert> + {type === 'question' && + info && + reviewInfo && + 'content' in reviewInfo && ( + <DiffContent + className="mt-2" + objectType={type} + oldData={{ + title: info.title, + original_text: info.content, + tags: info.tags, + }} + newData={{ + title: reviewInfo.title, + original_text: reviewInfo.content, + tags: reviewInfo.tags, + }} + /> + )} + {type === 'answer' && + info && + reviewInfo && + 'content' in reviewInfo && ( <DiffContent className="mt-2" objectType={type} newData={{ - original_text: reviewInfo.original_text, + original_text: reviewInfo.content, }} oldData={{ original_text: info.content, }} - opts={{ showTitle: false, showTagUrlSlug: false }} /> )} - </Col> - <Col lg={{ span: 7, offset: 1 }}> - <Stack direction="horizontal" gap={2} className="mt-4"> - <Button - variant="outline-primary" - disabled={isLoading} - onClick={handlingApprove}> - {t('approve', { keyPrefix: 'btns' })} - </Button> - <Button - variant="outline-primary" - disabled={isLoading} - onClick={handlingReject}> - {t('reject', { keyPrefix: 'btns' })} - </Button> - <Button - variant="outline-primary" - disabled={isLoading} - onClick={handlingSkip}> - {t('skip', { keyPrefix: 'btns' })} - </Button> - </Stack> - </Col> + {type === 'tag' && info && reviewInfo && ( + <DiffContent + className="mt-2" + objectType={type} + newData={{ + original_text: reviewInfo.original_text, + }} + oldData={{ + original_text: info.content, + }} + opts={{ showTitle: false, showTagUrlSlug: false }} + /> + )} + <Stack direction="horizontal" gap={2} className="mt-4"> + <Button + variant="outline-primary" + disabled={isLoading} + onClick={handlingApprove}> + {t('approve', { keyPrefix: 'btns' })} + </Button> + <Button + variant="outline-primary" + disabled={isLoading} + onClick={handlingReject}> + {t('reject', { keyPrefix: 'btns' })} + </Button> + <Button + variant="outline-primary" + disabled={isLoading} + onClick={handlingSkip}> + {t('skip', { keyPrefix: 'btns' })} + </Button> + </Stack> </> )} - {noTasks && ( - <Col lg={{ span: 7, offset: 1 }}> - <Empty>{t('empty')}</Empty> - </Col> - )} - </Row> - </Container> + {noTasks && <Empty>{t('empty')}</Empty>} + </Col> + + <Col className="page-right-side mt-4 mt-xl-0" /> + </Row> ); }; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index e911f941..935a5f2e 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Container, Row, Col, ListGroup } from 'react-bootstrap'; +import { Row, Col, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; @@ -39,36 +38,34 @@ const Index = () => { title: pageTitle, }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-3"> - <Head data={extra} /> - <SearchHead sort={order} count={count} /> - <ListGroup className="rounded-0 mb-5"> - {isLoading ? ( - <ListLoader /> - ) : ( - list?.map((item) => { - return <SearchItem key={item.object.id} data={item} />; - }) - )} - </ListGroup> + <Row className="pt-4 mb-5"> + <Col className="flex-auto"> + <Head data={extra} /> + <SearchHead sort={order} count={count} /> + <ListGroup className="rounded-0 mb-5"> + {isLoading ? ( + <ListLoader /> + ) : ( + list?.map((item) => { + return <SearchItem key={item.object.id} data={item} />; + }) + )} + </ListGroup> - {!isLoading && !list?.length && <Empty />} + {!isLoading && !list?.length && <Empty />} - <div className="d-flex justify-content-center"> - <Pagination - currentPage={Number(page)} - pageSize={20} - totalSize={count} - /> - </div> - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> - <Tips /> - </Col> - </Row> - </Container> + <div className="d-flex justify-content-center"> + <Pagination + currentPage={Number(page)} + pageSize={20} + totalSize={count} + /> + </div> + </Col> + <Col className="page-right-side mt-4 mt-xl-0"> + <Tips /> + </Col> + </Row> ); }; diff --git a/ui/src/pages/SideNavLayout/index.tsx b/ui/src/pages/SideNavLayout/index.tsx new file mode 100644 index 00000000..45ec3d6b --- /dev/null +++ b/ui/src/pages/SideNavLayout/index.tsx @@ -0,0 +1,22 @@ +import { FC, memo } from 'react'; +import { Container, Row, Col } from 'react-bootstrap'; +import { Outlet } from 'react-router-dom'; + +import { SideNav } from '@/components'; + +import '@/common/sideNavLayout.scss'; + +const Index: FC = () => { + return ( + <Container> + <Row> + <SideNav /> + <Col xl={10} lg={9} md={12}> + <Outlet /> + </Col> + </Row> + </Container> + ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Tags/Create/index.tsx b/ui/src/pages/Tags/Create/index.tsx index d87ec163..a3bef8fe 100644 --- a/ui/src/pages/Tags/Create/index.tsx +++ b/ui/src/pages/Tags/Create/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; +import { Row, Col, Form, Button, Card } from 'react-bootstrap'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -139,14 +139,10 @@ const Index = () => { }); return ( - <Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> - <Row className="justify-content-center"> - <Col xxl={10} md={12}> - <h3 className="mb-4">{t('title')}</h3> - </Col> - </Row> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> + <div className="pt-4 mb-5"> + <h3 className="mb-4">{t('title')}</h3> + <Row> + <Col className="flex-auto"> <Form noValidate onSubmit={handleSubmit}> <Form.Group controlId="display_name" className="mb-3"> <Form.Label>{t('form.fields.display_name.label')}</Form.Label> @@ -208,7 +204,7 @@ const Index = () => { </div> </Form> </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> + <Col className="page-right-side mt-4 mt-xl-0"> <Card> <Card.Header> {t('title', { keyPrefix: 'how_to_format' })} @@ -222,7 +218,7 @@ const Index = () => { </Card> </Col> </Row> - </Container> + </div> ); }; diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 37daf4a8..5fbf9973 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useState } from 'react'; -import { Container, Row, Col, Button } from 'react-bootstrap'; +import { Row, Col, Button } from 'react-bootstrap'; import { useParams, Link, @@ -100,64 +100,62 @@ const Questions: FC = () => { keywords: keywords.join(','), }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12}> - {isLoading || listLoading ? ( - <div className="tag-box mb-5 placeholder-glow"> - <div className="mb-3 h3 placeholder" style={{ width: '120px' }} /> - <p - className="placeholder w-100 d-block align-top" - style={{ height: '24px' }} - /> + <Row className="pt-4 mb-5"> + <Col className="flex-auto"> + {isLoading || listLoading ? ( + <div className="tag-box mb-5 placeholder-glow"> + <div className="mb-3 h3 placeholder" style={{ width: '120px' }} /> + <p + className="placeholder w-100 d-block align-top" + style={{ height: '24px' }} + /> - <div - className="placeholder d-block align-top" - style={{ height: '38px', width: '100px' }} - /> + <div + className="placeholder d-block align-top" + style={{ height: '38px', width: '100px' }} + /> + </div> + ) : ( + <div className="tag-box mb-5"> + <h3 className="mb-3"> + <Link + to={pathFactory.tagLanding(tagInfo.slug_name)} + replace + className="link-dark"> + {tagInfo.display_name} + </Link> + </h3> + + <p className="text-break"> + {escapeRemove(tagInfo.excerpt) || t('no_desc')} + <Link to={pathFactory.tagInfo(curTagName)} className="ms-1"> + [{t('more')}] + </Link> + </p> + + <div className="box-ft"> + {tagInfo.is_follower ? ( + <Button variant="primary" onClick={() => toggleFollow()}> + {t('button_following')} + </Button> + ) : ( + <Button + variant="outline-primary" + onClick={() => toggleFollow()}> + {t('button_follow')} + </Button> + )} </div> - ) : ( - <div className="tag-box mb-5"> - <h3 className="mb-3"> - <Link - to={pathFactory.tagLanding(tagInfo.slug_name)} - replace - className="link-dark"> - {tagInfo.display_name} - </Link> - </h3> - - <p className="text-break"> - {escapeRemove(tagInfo.excerpt) || t('no_desc')} - <Link to={pathFactory.tagInfo(curTagName)} className="ms-1"> - [{t('more')}] - </Link> - </p> - - <div className="box-ft"> - {tagInfo.is_follower ? ( - <Button variant="primary" onClick={() => toggleFollow()}> - {t('button_following')} - </Button> - ) : ( - <Button - variant="outline-primary" - onClick={() => toggleFollow()}> - {t('button_follow')} - </Button> - )} - </div> - </div> - )} - <QuestionList source="tag" data={listData} isLoading={listLoading} /> - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> - <CustomSidebar /> - <FollowingTags /> - <HotQuestions /> - </Col> - </Row> - </Container> + </div> + )} + <QuestionList source="tag" data={listData} isLoading={listLoading} /> + </Col> + <Col className="page-right-side mt-4 mt-xl-0"> + <CustomSidebar /> + <FollowingTags /> + <HotQuestions /> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index aa895234..8423e5d6 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; +import { Row, Col, Form, Button, Card } from 'react-bootstrap'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -188,14 +188,10 @@ const Index = () => { title: t('edit_tag', { keyPrefix: 'page_title' }), }); return ( - <Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> - <Row className="justify-content-center"> - <Col xxl={10} md={12}> - <h3 className="mb-4">{t('title')}</h3> - </Col> - </Row> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> + <div className="pt-4 mb-5"> + <h3 className="mb-4">{t('title')}</h3> + <Row> + <Col className="flex-auto"> <Form noValidate onSubmit={handleSubmit}> <Form.Group controlId="revision" className="mb-3"> <Form.Label>{t('form.fields.revision.label')}</Form.Label> @@ -291,7 +287,7 @@ const Index = () => { </div> </Form> </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> + <Col className="page-right-side mt-4 mt-xl-0"> <Card> <Card.Header> {t('title', { keyPrefix: 'how_to_format' })} @@ -305,7 +301,7 @@ const Index = () => { </Card> </Col> </Row> - </Container> + </div> ); }; diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 3f747f49..f28c6403 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Container, Row, Col, Button, Card } from 'react-bootstrap'; +import { Row, Col, Button, Card } from 'react-bootstrap'; import { useParams, useNavigate, Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -132,132 +132,130 @@ const TagIntroduction = () => { }; return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12}> - <h3 className="mb-3"> - <Link - to={pathFactory.tagLanding(tagInfo.slug_name)} - replace - className="link-dark"> - {tagInfo.display_name} - </Link> - </h3> + <Row className="pt-4 mb-5"> + <Col className="flex-auto"> + <h3 className="mb-3"> + <Link + to={pathFactory.tagLanding(tagInfo.slug_name)} + replace + className="link-dark"> + {tagInfo.display_name} + </Link> + </h3> - <div className="text-secondary mb-4 fs-14"> - <FormatTime preFix={t('created_at')} time={tagInfo.created_at} /> - <FormatTime - preFix={t('edited_at')} - className="ms-3" - time={tagInfo.updated_at} - /> - </div> - - <div - className="content text-break fmt" - dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }} + <div className="text-secondary mb-4 fs-14"> + <FormatTime preFix={t('created_at')} time={tagInfo.created_at} /> + <FormatTime + preFix={t('edited_at')} + className="ms-3" + time={tagInfo.updated_at} /> - <div className="mt-4"> - {tagInfo?.member_actions.map((action, index) => { - return ( - <Button - key={action.name} - variant="link" - className={classNames( - 'link-secondary btn-no-border p-0 fs-14', - index > 0 && 'ms-3', - )} - onClick={() => onAction(action)}> - {action.name} - </Button> - ); - })} - {isLogged && ( - <Link - to={`/tags/${tagInfo?.tag_id}/timeline`} + </div> + + <div + className="content text-break fmt" + dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }} + /> + <div className="mt-4"> + {tagInfo?.member_actions.map((action, index) => { + return ( + <Button + key={action.name} + variant="link" className={classNames( 'link-secondary btn-no-border p-0 fs-14', - tagInfo?.member_actions?.length > 0 && 'ms-3', - )}> - {t('history')} - </Link> - )} - </div> - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> - <Card> - <Card.Header className="d-flex justify-content-between"> - <span>{t('synonyms.title')}</span> - {isEdit ? ( - <Button - variant="link" - className="p-0 btn-no-border" - onClick={handleSave}> - {t('synonyms.btn_save')} - </Button> - ) : synonymsData?.member_actions?.find( - (v) => v.action === 'edit', - ) ? ( - <Button - variant="link" - className="p-0 btn-no-border" - onClick={handleEdit}> - {t('synonyms.btn_edit')} - </Button> - ) : null} - </Card.Header> - <Card.Body> - {isEdit && ( - <> - <div className="mb-3"> - {t('synonyms.text')}{' '} - <Tag - data={{ - slug_name: tagName || '', - main_tag_slug_name: '', - display_name: - tagInfo?.display_name || tagInfo?.slug_name || '', - recommend: false, - reserved: false, - }} - /> - </div> - <TagSelector - value={synonymsData?.synonyms} - onChange={handleTagsChange} - hiddenDescription + index > 0 && 'ms-3', + )} + onClick={() => onAction(action)}> + {action.name} + </Button> + ); + })} + {isLogged && ( + <Link + to={`/tags/${tagInfo?.tag_id}/timeline`} + className={classNames( + 'link-secondary btn-no-border p-0 fs-14', + tagInfo?.member_actions?.length > 0 && 'ms-3', + )}> + {t('history')} + </Link> + )} + </div> + </Col> + <Col className="page-right-side mt-4 mt-xl-0"> + <Card> + <Card.Header className="d-flex justify-content-between"> + <span>{t('synonyms.title')}</span> + {isEdit ? ( + <Button + variant="link" + className="p-0 btn-no-border" + onClick={handleSave}> + {t('synonyms.btn_save')} + </Button> + ) : synonymsData?.member_actions?.find( + (v) => v.action === 'edit', + ) ? ( + <Button + variant="link" + className="p-0 btn-no-border" + onClick={handleEdit}> + {t('synonyms.btn_edit')} + </Button> + ) : null} + </Card.Header> + <Card.Body> + {isEdit && ( + <> + <div className="mb-3"> + {t('synonyms.text')}{' '} + <Tag + data={{ + slug_name: tagName || '', + main_tag_slug_name: '', + display_name: + tagInfo?.display_name || tagInfo?.slug_name || '', + recommend: false, + reserved: false, + }} /> + </div> + <TagSelector + value={synonymsData?.synonyms} + onChange={handleTagsChange} + hiddenDescription + /> + </> + )} + {!isEdit && + (synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? ( + <div className="m-n1"> + {synonymsData.synonyms.map((item) => { + return ( + <Tag key={item.tag_id} className="m-1" data={item} /> + ); + })} + </div> + ) : ( + <> + <div className="text-muted mb-3">{t('synonyms.empty')}</div> + {synonymsData?.member_actions?.find( + (v) => v.action === 'edit', + ) && ( + <Button + variant="outline-primary" + size="sm" + onClick={handleEdit}> + {t('synonyms.btn_add')} + </Button> + )} </> - )} - {!isEdit && - (synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? ( - <div className="m-n1"> - {synonymsData.synonyms.map((item) => { - return ( - <Tag key={item.tag_id} className="m-1" data={item} /> - ); - })} - </div> - ) : ( - <> - <div className="text-muted mb-3">{t('synonyms.empty')}</div> - {synonymsData?.member_actions?.find( - (v) => v.action === 'edit', - ) && ( - <Button - variant="outline-primary" - size="sm" - onClick={handleEdit}> - {t('synonyms.btn_add')} - </Button> - )} - </> - ))} - </Card.Body> - </Card> - </Col> - </Row> - </Container> + ))} + </Card.Body> + </Card> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index b0e5b361..1efc2f70 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -1,13 +1,5 @@ import { useState } from 'react'; -import { - Container, - Row, - Col, - Card, - Button, - Form, - Stack, -} from 'react-bootstrap'; +import { Row, Col, Card, Button, Form, Stack } from 'react-bootstrap'; import { useSearchParams, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -60,90 +52,89 @@ const Tags = () => { title: t('tags', { keyPrefix: 'page_title' }), }); return ( - <Container className="py-3 my-3"> - <Row className="mb-4 d-flex justify-content-center"> - <Col xxl={10} sm={12}> - <h3 className="mb-4">{t('title')}</h3> - <div className="d-block d-sm-flex justify-content-between align-items-center flex-wrap"> - <Stack direction="horizontal" gap={3} className="mb-3 mb-sm-0"> - <Form> - <Form.Group controlId="formBasicEmail"> - <Form.Control - value={searchTag} - placeholder={t('search_placeholder')} - type="text" - onChange={handleChange} - size="sm" - /> - </Form.Group> - </Form> - {role_id === 2 || role_id === 3 ? ( - <Link - className="btn btn-outline-primary btn-sm" - to="/tags/create"> - {t('title', { keyPrefix: 'tag_modal' })} - </Link> - ) : null} - </Stack> - <QueryGroup - data={sortBtns} - currentSort={sort || 'popular'} - sortKey="sort" - i18nKeyPrefix="tags.sort_buttons" - /> - </div> - </Col> + <Row className="py-4 mb-4"> + <Col xxl={12}> + <h3 className="mb-4">{t('title')}</h3> + <div className="d-block d-sm-flex justify-content-between align-items-center flex-wrap"> + <Stack direction="horizontal" gap={3} className="mb-3 mb-sm-0"> + <Form> + <Form.Group controlId="formBasicEmail"> + <Form.Control + value={searchTag} + placeholder={t('search_placeholder')} + type="text" + onChange={handleChange} + size="sm" + /> + </Form.Group> + </Form> + {role_id === 2 || role_id === 3 ? ( + <Link + className="btn btn-outline-primary btn-sm" + to="/tags/create"> + {t('title', { keyPrefix: 'tag_modal' })} + </Link> + ) : null} + </Stack> + <QueryGroup + data={sortBtns} + currentSort={sort || 'popular'} + sortKey="sort" + i18nKeyPrefix="tags.sort_buttons" + /> + </div> + </Col> - <Col className="mt-4" xxl={10} sm={12}> - <Row> - {isLoading ? ( - <TagsLoader /> - ) : ( - tags?.list?.map((tag) => ( - <Col - key={tag.slug_name} - xs={12} - lg={3} - md={4} - sm={6} - className="mb-4"> - <Card className="h-100"> - <Card.Body className="d-flex flex-column align-items-start"> - <Tag className="mb-3" data={tag} /> + <Col className="mt-4" xxl={12}> + <Row> + {isLoading ? ( + <TagsLoader /> + ) : ( + tags?.list?.map((tag) => ( + <Col + key={tag.slug_name} + xl={3} + lg={4} + md={4} + sm={6} + xs={12} + className="mb-4"> + <Card className="h-100"> + <Card.Body className="d-flex flex-column align-items-start"> + <Tag className="mb-3" data={tag} /> - <div className="fs-14 flex-fill text-break text-wrap text-truncate-3 reset-p mb-3"> - {escapeRemove(tag.excerpt)} - </div> - <div className="d-flex align-items-center"> - <Button - className={`me-2 ${tag.is_follower ? 'active' : ''}`} - variant="outline-primary" - size="sm" - onClick={() => handleFollow(tag)}> - {tag.is_follower - ? t('button_following') - : t('button_follow')} - </Button> - <span className="text-secondary fs-14 text-nowrap"> - {formatCount(tag.question_count)} {t('tag_label')} - </span> - </div> - </Card.Body> - </Card> - </Col> - )) - )} - </Row> - <div className="d-flex justify-content-center"> - <Pagination - currentPage={page} - totalSize={tags?.count || 0} - pageSize={pageSize} - /> - </div> - </Col> - </Row> - </Container> + <div className="fs-14 flex-fill text-break text-wrap text-truncate-3 reset-p mb-3"> + {escapeRemove(tag.excerpt)} + </div> + <div className="d-flex align-items-center"> + <Button + className={`me-2 ${tag.is_follower ? 'active' : ''}`} + variant="outline-primary" + size="sm" + onClick={() => handleFollow(tag)}> + {tag.is_follower + ? t('button_following') + : t('button_follow')} + </Button> + <span className="text-secondary fs-14 text-nowrap"> + {formatCount(tag.question_count)} {t('tag_label')} + </span> + </div> + </Card.Body> + </Card> + </Col> + )) + )} + </Row> + <div className="d-flex justify-content-center"> + <Pagination + currentPage={page} + totalSize={tags?.count || 0} + pageSize={pageSize} + /> + </div> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Timeline/index.tsx b/ui/src/pages/Timeline/index.tsx index a77f7e6a..99266e38 100644 --- a/ui/src/pages/Timeline/index.tsx +++ b/ui/src/pages/Timeline/index.tsx @@ -1,5 +1,5 @@ import { FC, useState, useEffect } from 'react'; -import { Container, Row, Col, Form, Table } from 'react-bootstrap'; +import { Form, Table } from 'react-bootstrap'; import { Link, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -79,54 +79,50 @@ const Index: FC = () => { title: pageTitle, }); return ( - <Container className="py-3"> - <Row className="py-3 justify-content-center"> - <Col xxl={10}> - <h5 className="mb-4"> - {timelineData?.object_info.object_type === 'tag' - ? t('tag_title') - : t('title')}{' '} - <Link to={linkUrl}>{timelineData?.object_info?.title}</Link> - </h5> - {timelineData?.object_info.object_type !== 'tag' && ( - <Form.Check - className="mb-4" - type="switch" - id="custom-switch" - label={t('show_votes')} - checked={showVotes} - onChange={(e) => handleSwitch(e.target.checked)} - /> - )} - <Table hover> - <thead> - <tr> - <th style={{ width: '20%' }}>{t('datetime')}</th> - <th style={{ width: '15%' }}>{t('type')}</th> - <th style={{ width: '19%' }}>{t('by')}</th> - <th>{t('comment')}</th> - </tr> - </thead> - <tbody> - {timelineData?.timeline?.map((item) => { - return ( - <HistoryItem - data={item} - objectInfo={timelineData?.object_info} - key={item.activity_id} - isAdmin={role_id === 2} - revisionList={revisionList} - /> - ); - })} - </tbody> - </Table> - {!isLoading && Number(timelineData?.timeline?.length) <= 0 && ( - <Empty>{t('no_data')}</Empty> - )} - </Col> - </Row> - </Container> + <div className="py-4 mb-5"> + <h5 className="mb-4"> + {timelineData?.object_info.object_type === 'tag' + ? t('tag_title') + : t('title')}{' '} + <Link to={linkUrl}>{timelineData?.object_info?.title}</Link> + </h5> + {timelineData?.object_info.object_type !== 'tag' && ( + <Form.Check + className="mb-4" + type="switch" + id="custom-switch" + label={t('show_votes')} + checked={showVotes} + onChange={(e) => handleSwitch(e.target.checked)} + /> + )} + <Table hover> + <thead> + <tr> + <th style={{ width: '20%' }}>{t('datetime')}</th> + <th style={{ width: '15%' }}>{t('type')}</th> + <th style={{ width: '19%' }}>{t('by')}</th> + <th>{t('comment')}</th> + </tr> + </thead> + <tbody> + {timelineData?.timeline?.map((item) => { + return ( + <HistoryItem + data={item} + objectInfo={timelineData?.object_info} + key={item.activity_id} + isAdmin={role_id === 2} + revisionList={revisionList} + /> + ); + })} + </tbody> + </Table> + {!isLoading && Number(timelineData?.timeline?.length) <= 0 && ( + <Empty>{t('no_data')}</Empty> + )} + </div> ); }; diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index e903b4fb..8b2f8032 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; +import { Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; @@ -74,62 +74,60 @@ const Notifications = () => { title: t('notifications', { keyPrefix: 'page_title' }), }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> - <Col xxl={7} lg={8} sm={12}> - <h3 className="mb-4">{t('title')}</h3> - <div className="d-flex justify-content-between mb-3"> - <ButtonGroup size="sm"> - <Button - as="a" - href="/users/notifications/inbox" - variant="outline-secondary" - active={type === 'inbox'} - onClick={(evt) => handleTypeChange(evt, 'inbox')}> - {t('inbox')} - </Button> - <Button - as="a" - href="/users/notifications/achievement" - variant="outline-secondary" - active={type === 'achievement'} - onClick={(evt) => handleTypeChange(evt, 'achievement')}> - {t('achievement')} - </Button> - </ButtonGroup> + <Row className="pt-4 mb-5"> + <Col className="flex-auto"> + <h3 className="mb-4">{t('title')}</h3> + <div className="d-flex justify-content-between mb-3"> + <ButtonGroup size="sm"> <Button - size="sm" + as="a" + href="/users/notifications/inbox" variant="outline-secondary" - onClick={handleUnreadNotification}> - {t('all_read')} + active={type === 'inbox'} + onClick={(evt) => handleTypeChange(evt, 'inbox')}> + {t('inbox')} + </Button> + <Button + as="a" + href="/users/notifications/achievement" + variant="outline-secondary" + active={type === 'achievement'} + onClick={(evt) => handleTypeChange(evt, 'achievement')}> + {t('achievement')} + </Button> + </ButtonGroup> + <Button + size="sm" + variant="outline-secondary" + onClick={handleUnreadNotification}> + {t('all_read')} + </Button> + </div> + {type === 'inbox' && ( + <Inbox + data={notificationData} + handleReadNotification={handleReadNotification} + /> + )} + {type === 'achievement' && ( + <Achievements + data={notificationData} + handleReadNotification={handleReadNotification} + /> + )} + {(data?.count || 0) > PAGE_SIZE * page && ( + <div className="d-flex justify-content-center align-items-center py-3"> + <Button + variant="link" + className="btn-no-border" + onClick={handleLoadMore}> + {t('show_more')} </Button> </div> - {type === 'inbox' && ( - <Inbox - data={notificationData} - handleReadNotification={handleReadNotification} - /> - )} - {type === 'achievement' && ( - <Achievements - data={notificationData} - handleReadNotification={handleReadNotification} - /> - )} - {(data?.count || 0) > PAGE_SIZE * page && ( - <div className="d-flex justify-content-center align-items-center py-3"> - <Button - variant="link" - className="btn-no-border" - onClick={handleLoadMore}> - {t('show_more')} - </Button> - </div> - )} - </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" /> - </Row> - </Container> + )} + </Col> + <Col className="page-right-side" /> + </Row> ); }; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 41033a05..999aa2ed 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams, Link } from 'react-router-dom'; @@ -58,19 +58,19 @@ const Personal: FC = () => { }); return ( - <Container className="pt-4 mt-2 mb-5"> - <Row className="justify-content-center"> + <div className="pt-4 mb-5"> + <Row> {userInfo?.status !== 'normal' && userInfo?.status_msg && ( <Alert data={userInfo?.status_msg} /> )} - <Col xxl={7} lg={8} sm={12}> + <Col className="flex-auto"> <UserInfo data={userInfo as UserInfoRes} /> </Col> <Col xxl={3} lg={4} sm={12} - className="d-flex justify-content-start justify-content-md-end"> + className="page-right-side mt-4 mt-xl-0 d-flex justify-content-start justify-content-md-end"> {isSelf && ( <div className="mb-3"> <Link @@ -83,11 +83,9 @@ const Personal: FC = () => { </Col> </Row> - <Row className="justify-content-center"> - <Col xxl={10}> - <NavBar tabName={tabName} slug={username} isSelf={isSelf} /> - </Col> - <Col xxl={7} lg={8} sm={12}> + <Row> + <NavBar tabName={tabName} slug={username} isSelf={isSelf} /> + <Col className="flex-auto"> <Overview visible={tabName === 'overview'} introduction={userInfo?.bio_html || ''} @@ -120,7 +118,7 @@ const Personal: FC = () => { </div> )} </Col> - <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> + <Col className="page-right-side mt-4 mt-xl-0"> <h5 className="mb-3">{t('stats')}</h5> {userInfo?.created_at && ( <> @@ -137,7 +135,7 @@ const Personal: FC = () => { )} </Col> </Row> - </Container> + </div> ); }; export default Personal; diff --git a/ui/src/pages/Users/Settings/index.scss b/ui/src/pages/Users/Settings/index.scss new file mode 100644 index 00000000..7d9d8894 --- /dev/null +++ b/ui/src/pages/Users/Settings/index.scss @@ -0,0 +1,26 @@ +.settings-nav { + flex: none; + width: 20%; +} + +.settings-main { + flex: none; + width: 60%; +} + +// lg +@media screen and (max-width: 1199.9px) { + .settings-nav { + width: 30%; + } + .settings-main { + width: 70%; + } +} + +// sm +@media screen and (max-width: 767.9px) { + .settings-main { + width: 100%; + } +} diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 8b563246..807ec465 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -1,5 +1,5 @@ import { FC, memo } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; @@ -7,6 +7,8 @@ import { usePageTags } from '@/hooks'; import Nav from './components/Nav'; +import './index.scss'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', @@ -16,23 +18,14 @@ const Index: FC = () => { title: t('settings', { keyPrefix: 'page_title' }), }); return ( - <Container className="mt-4 mb-5 pb-5"> - <Row className="justify-content-center"> - <Col xxl={10} md={12}> - <h3 className="mb-4">{t('page_title', { keyPrefix: 'settings' })}</h3> - </Col> - </Row> - - <Row> - <Col xxl={1} /> - <Col md={3} lg={2} className="mb-3"> - <Nav /> - </Col> - <Col md={9} lg={6}> - <Outlet /> - </Col> - </Row> - </Container> + <Row className="mt-4 mb-5 pb-5"> + <Col className="settings-nav mb-4"> + <Nav /> + </Col> + <Col className="settings-main"> + <Outlet /> + </Col> + </Row> ); }; diff --git a/ui/src/pages/Users/index.tsx b/ui/src/pages/Users/index.tsx index b3c73303..b537f1c7 100644 --- a/ui/src/pages/Users/index.tsx +++ b/ui/src/pages/Users/index.tsx @@ -1,4 +1,4 @@ -import { Container, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { Fragment } from 'react'; @@ -22,61 +22,60 @@ const Users = () => { const keys = Object.keys(users); return ( - <Container className="py-3 my-3"> - <Row className="mb-4 d-flex justify-content-center"> - <Col xxl={10} sm={12}> - <h3 className="mb-4">{t('title')}</h3> - </Col> + <Row className="py-4 mb-4 d-flex justify-content-center"> + <Col xxl={12}> + <h3 className="mb-4">{t('title')}</h3> + </Col> - <Col xxl={10} sm={12}> - {keys.map((key, index) => { - if (users[key]?.length === 0) { - return null; - } - return ( - <Fragment key={key}> - <Row className="mb-4"> - <Col> - <h6 className="mb-0">{t(key)}</h6> - </Col> - </Row> - <Row className={index === keys.length - 1 ? '' : 'mb-4'}> - {users[key]?.map((user) => ( - <Col - key={user.username} - xs={12} - lg={3} - md={4} - sm={6} - className="mb-4"> - <div className="d-flex"> + <Col xxl={12}> + {keys.map((key, index) => { + if (users[key]?.length === 0) { + return null; + } + return ( + <Fragment key={key}> + <Row className="mb-4"> + <Col> + <h6 className="mb-0">{t(key)}</h6> + </Col> + </Row> + <Row className={index === keys.length - 1 ? '' : 'mb-4'}> + {users[key]?.map((user) => ( + <Col + key={user.username} + xl={3} + lg={4} + md={4} + sm={6} + xs={12} + className="mb-4"> + <div className="d-flex"> + <Link to={`/users/${user.username}`}> + <Avatar + size="48px" + avatar={user?.avatar} + searchStr="s=96" + /> + </Link> + <div className="ms-2"> <Link to={`/users/${user.username}`}> - <Avatar - size="48px" - avatar={user?.avatar} - searchStr="s=96" - /> + {user.display_name} </Link> - <div className="ms-2"> - <Link to={`/users/${user.username}`}> - {user.display_name} - </Link> - <div className="text-secondary fs-14"> - {key === 'users_with_the_most_vote' - ? `${user.vote_count} ${t('votes')}` - : `${user.rank} ${t('reputation')}`} - </div> + <div className="text-secondary fs-14"> + {key === 'users_with_the_most_vote' + ? `${user.vote_count} ${t('votes')}` + : `${user.rank} ${t('reputation')}`} </div> </div> - </Col> - ))} - </Row> - </Fragment> - ); - })} - </Col> - </Row> - </Container> + </div> + </Col> + ))} + </Row> + </Fragment> + ); + })} + </Col> + </Row> ); }; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 00ea0aab..6019b7ee 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -40,127 +40,161 @@ const routes: RouteNode[] = [ children: [ // question and answer { - index: true, - page: 'pages/Questions', - }, - { - path: 'questions', - page: 'pages/Questions', - }, - { - path: 'questions/ask', - page: 'pages/Questions/Ask', - guard: () => { - return guard.activated(); - }, - }, - { - path: 'questions/:qid', - page: 'pages/Questions/Detail', - }, - { - path: 'questions/:qid/:slugPermalink', - page: 'pages/Questions/Detail', - }, - { - path: 'questions/:qid/:slugPermalink/:aid', - page: 'pages/Questions/Detail', - }, - { - path: 'posts/:qid/edit', - page: 'pages/Questions/Ask', - guard: () => { - return guard.activated(); - }, - }, - { - path: 'posts/:qid/:aid/edit', - page: 'pages/Questions/EditAnswer', - loader: async ({ params }) => { - const ret = await editCheck(params.aid as string, true); - return ret; - }, - guard: (args) => { - return isEditable(args); - }, - }, - { - path: '/search', - page: 'pages/Search', - }, - // tags - { - path: 'tags', - page: 'pages/Tags', - }, - { - path: 'tags/create', - page: 'pages/Tags/Create', - guard: () => { - return guard.isAdminOrModerator(); - }, - }, - { - path: 'tags/:tagName', - page: 'pages/Tags/Detail', - }, - { - path: 'tags/:tagName/info', - page: 'pages/Tags/Info', - }, - { - path: 'tags/:tagId/edit', - page: 'pages/Tags/Edit', - guard: () => { - return guard.activated(); - }, - }, - // for users - { - path: 'users', - page: 'pages/Users', - }, - { - path: 'users/:username', - page: 'pages/Users/Personal', - }, - { - path: 'users/:username/:tabName', - page: 'pages/Users/Personal', - }, - { - path: 'users/settings', - page: 'pages/Users/Settings', - guard: () => { - return guard.logged(); - }, + // side nav layout + path: '/', + page: 'pages/SideNavLayout', children: [ { index: true, - page: 'pages/Users/Settings/Profile', + page: 'pages/Questions', }, { - path: 'profile', - page: 'pages/Users/Settings/Profile', + path: 'questions', + page: 'pages/Questions', }, { - path: 'notify', - page: 'pages/Users/Settings/Notification', + path: 'questions/ask', + page: 'pages/Questions/Ask', + guard: () => { + return guard.activated(); + }, }, { - path: 'account', - page: 'pages/Users/Settings/Account', + path: 'posts/:qid/edit', + page: 'pages/Questions/Ask', + guard: () => { + return guard.activated(); + }, }, { - path: 'interface', - page: 'pages/Users/Settings/Interface', + path: 'posts/:qid/:aid/edit', + page: 'pages/Questions/EditAnswer', + loader: async ({ params }) => { + const ret = await editCheck(params.aid as string, true); + return ret; + }, + guard: (args) => { + return isEditable(args); + }, + }, + { + path: 'questions/:qid', + page: 'pages/Questions/Detail', + }, + { + path: 'questions/:qid/:slugPermalink', + page: 'pages/Questions/Detail', + }, + { + path: 'questions/:qid/:slugPermalink/:aid', + page: 'pages/Questions/Detail', + }, + { + path: '/search', + page: 'pages/Search', + }, + // tags + { + path: 'tags', + page: 'pages/Tags', + }, + { + path: 'tags/create', + page: 'pages/Tags/Create', + guard: () => { + return guard.isAdminOrModerator(); + }, + }, + { + path: 'tags/:tagName', + page: 'pages/Tags/Detail', + }, + { + path: 'tags/:tagName/info', + page: 'pages/Tags/Info', + }, + { + path: 'tags/:tagId/edit', + page: 'pages/Tags/Edit', + guard: () => { + return guard.activated(); + }, + }, + // for users + { + path: 'users', + page: 'pages/Users', + }, + { + path: 'users/:username', + page: 'pages/Users/Personal', + }, + { + path: 'users/:username/:tabName', + page: 'pages/Users/Personal', + }, + { + path: 'users/settings', + page: 'pages/Users/Settings', + guard: () => { + return guard.logged(); + }, + children: [ + { + index: true, + page: 'pages/Users/Settings/Profile', + }, + { + path: 'profile', + page: 'pages/Users/Settings/Profile', + }, + { + path: 'notify', + page: 'pages/Users/Settings/Notification', + }, + { + path: 'account', + page: 'pages/Users/Settings/Account', + }, + { + path: 'interface', + page: 'pages/Users/Settings/Interface', + }, + ], + }, + { + path: 'users/notifications/:type', + page: 'pages/Users/Notifications', + }, + { + path: '/posts/:qid/timeline', + page: 'pages/Timeline', + guard: () => { + return guard.logged(); + }, + }, + { + path: '/posts/:qid/:aid/timeline', + page: 'pages/Timeline', + guard: () => { + return guard.logged(); + }, + }, + { + path: '/tags/:tid/timeline', + page: 'pages/Timeline', + guard: () => { + return guard.logged(); + }, + }, + // for review + { + path: 'review', + page: 'pages/Review', }, ], }, - { - path: 'users/notifications/:type', - page: 'pages/Users/Notifications', - }, + { path: 'users/login', page: 'pages/Users/Login', @@ -243,27 +277,6 @@ const routes: RouteNode[] = [ path: '/users/auth-landing', page: 'pages/Users/AuthCallback', }, - { - path: '/posts/:qid/timeline', - page: 'pages/Timeline', - guard: () => { - return guard.logged(); - }, - }, - { - path: '/posts/:qid/:aid/timeline', - page: 'pages/Timeline', - guard: () => { - return guard.logged(); - }, - }, - { - path: '/tags/:tid/timeline', - page: 'pages/Timeline', - guard: () => { - return guard.logged(); - }, - }, // for admin { path: 'admin', @@ -374,11 +387,6 @@ const routes: RouteNode[] = [ path: '/user-center/auth-failed', page: 'pages/UserCenter/AuthFailed', }, - // for review - { - path: 'review', - page: 'pages/Review', - }, { path: '*', page: 'pages/404', diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 4740e25f..879b1f33 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -11,6 +11,7 @@ import customizeStore from './customize'; import themeSettingStore from './themeSetting'; import loginToContinueStore from './loginToContinue'; import errorCodeStore from './errorCode'; +import sideNavStore from './sideNav'; export { toastStore, @@ -26,4 +27,5 @@ export { loginToContinueStore, errorCodeStore, userCenterStore, + sideNavStore, }; diff --git a/ui/src/stores/sideNav.ts b/ui/src/stores/sideNav.ts new file mode 100644 index 00000000..3f9a2f13 --- /dev/null +++ b/ui/src/stores/sideNav.ts @@ -0,0 +1,32 @@ +import create from 'zustand'; + +type reviewData = { + can_revision: boolean; + revision: number; +}; + +interface ErrorCodeType { + visible: boolean; + can_revision: boolean; + revision: number; + updateVisible: () => void; + updateReiview: (params: reviewData) => void; +} + +const Index = create<ErrorCodeType>((set) => ({ + visible: false, + can_revision: false, + revision: 0, + updateVisible: () => { + set((state) => { + return { visible: !state.visible }; + }); + }, + updateReiview: (params: reviewData) => { + set(() => { + return { ...params }; + }); + }, +})); + +export default Index;