feat: add side nav

This commit is contained in:
shuai 2023-05-22 15:24:11 +08:00
parent e1e6627357
commit 6f63a0a0cd
29 changed files with 1048 additions and 902 deletions

View File

@ -708,6 +708,8 @@ ui:
logout: Log out logout: Log out
admin: Admin admin: Admin
review: Review review: Review
bookmark: Bookmarks
moderation: Moderation
search: search:
placeholder: Search placeholder: Search
footer: footer:
@ -903,7 +905,7 @@ ui:
Views: Viewed Views: Viewed
Follow: Follow Follow: Follow
Following: Following Following: Following
follow_tip: Follow this question to receive notifications. follow_tip: Follow this question to receive notifications
answered: answered answered: answered
closed_in: Closed in closed_in: Closed in
show_exist: Show existing question. show_exist: Show existing question.

View File

@ -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;
}
}

View File

@ -63,29 +63,16 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
onClick={handleLinkClick}> onClick={handleLinkClick}>
{t('header.nav.profile')} {t('header.nav.profile')}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item
href={`/users/${userInfo.username}/bookmarks`}
onClick={handleLinkClick}>
{t('header.nav.bookmark')}
</Dropdown.Item>
<Dropdown.Item <Dropdown.Item
href="/users/settings/profile" href="/users/settings/profile"
onClick={handleLinkClick}> onClick={handleLinkClick}>
{t('header.nav.setting')} {t('header.nav.setting')}
</Dropdown.Item> </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.Divider />
<Dropdown.Item onClick={logOut}> <Dropdown.Item onClick={logOut}>
{t('header.nav.logout')} {t('header.nav.logout')}

View File

@ -52,10 +52,14 @@
&.theme-light { &.theme-light {
background: linear-gradient(180deg, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.95) 100%); 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 { #header {
.nav-grow { .nav-grow {
flex-grow: 1!important; flex-grow: 1!important;
@ -65,8 +69,8 @@
display: flex!important; display: flex!important;
} }
.w-75 { .maxw-400 {
width: 100% !important; max-width: 100%;
} }
} }

View File

@ -11,7 +11,6 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
useSearchParams, useSearchParams,
NavLink,
Link, Link,
useNavigate, useNavigate,
useLocation, useLocation,
@ -27,6 +26,7 @@ import {
brandingStore, brandingStore,
loginSettingStore, loginSettingStore,
themeSettingStore, themeSettingStore,
sideNavStore,
} from '@/stores'; } from '@/stores';
import { logout, useQueryNotificationStatus } from '@/services'; import { logout, useQueryNotificationStatus } from '@/services';
@ -45,6 +45,7 @@ const Header: FC = () => {
const siteInfo = siteInfoStore((state) => state.siteInfo); const siteInfo = siteInfoStore((state) => state.siteInfo);
const brandingInfo = brandingStore((state) => state.branding); const brandingInfo = brandingStore((state) => state.branding);
const loginSetting = loginSettingStore((state) => state.login); const loginSetting = loginSettingStore((state) => state.login);
const { updateReiview, updateVisible } = sideNavStore();
const { data: redDot } = useQueryNotificationStatus(); const { data: redDot } = useQueryNotificationStatus();
/** /**
* Automatically append `tag` information when creating a question * Automatically append `tag` information when creating a question
@ -55,6 +56,13 @@ const Header: FC = () => {
askUrl = `${askUrl}?tags=${tagMatch.params.slugName}`; askUrl = `${askUrl}?tags=${tagMatch.params.slugName}`;
} }
useEffect(() => {
updateReiview({
can_revision: Boolean(redDot?.can_revision),
revision: Number(redDot?.revision),
});
}, [redDot]);
const handleInput = (val) => { const handleInput = (val) => {
setSearch(val); setSearch(val);
}; };
@ -106,10 +114,13 @@ const Header: FC = () => {
aria-controls="navBarContent" aria-controls="navBarContent"
className="answer-navBar me-2" className="answer-navBar me-2"
id="navBarToggle" id="navBarToggle"
onClick={() => {
updateVisible();
}}
/> />
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap"> <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 ? ( {brandingInfo.logo ? (
<> <>
<img <img
@ -159,26 +170,11 @@ const Header: FC = () => {
</div> </div>
<Navbar.Collapse id="navBarContent" className="me-auto"> <Navbar.Collapse id="navBarContent" className="me-auto">
<hr className="hr lg-none mb-2" style={{ marginTop: '12px' }} /> <hr className="hr lg-none mb-3" style={{ marginTop: '12px' }} />
<Col md={4}> <Col lg={8} className="ps-0">
<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">
<Form <Form
action="/search" action="/search"
className="w-75 px-0 px-lg-2" className="w-100 maxw-400"
onSubmit={handleSearch}> onSubmit={handleSearch}>
<FormControl <FormControl
placeholder={t('header.search.placeholder')} placeholder={t('header.search.placeholder')}

View File

@ -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%;
}
}
}

View File

@ -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;

View File

@ -40,6 +40,7 @@ import HotQuestions from './HotQuestions';
import HttpErrorContent from './HttpErrorContent'; import HttpErrorContent from './HttpErrorContent';
import CustomSidebar from './CustomSidebar'; import CustomSidebar from './CustomSidebar';
import ImgViewer from './ImgViewer'; import ImgViewer from './ImgViewer';
import SideNav from './SideNav';
export { export {
Avatar, Avatar,
@ -86,5 +87,6 @@ export {
HttpErrorContent, HttpErrorContent,
CustomSidebar, CustomSidebar,
ImgViewer, ImgViewer,
SideNav,
}; };
export type { EditorRef, JSONSchema, UISchema }; export type { EditorRef, JSONSchema, UISchema };

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; 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 { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -320,14 +320,10 @@ const Ask = () => {
title: pageTitle, title: pageTitle,
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <div className="pt-4 mb-5">
<Row className="justify-content-center"> <h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
<Col xxl={10} md={12}> <Row>
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3> <Col className="flex-auto">
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
{isEdit && ( {isEdit && (
<Form.Group controlId="revision" className="mb-3"> <Form.Group controlId="revision" className="mb-3">
@ -491,7 +487,7 @@ const Ask = () => {
)} )}
</Form> </Form>
</Col> </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>
<Card.Header> <Card.Header>
{t('title', { keyPrefix: 'how_to_format' })} {t('title', { keyPrefix: 'how_to_format' })}
@ -505,7 +501,7 @@ const Ask = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Container> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { import {
useParams, useParams,
useSearchParams, useSearchParams,
@ -198,69 +198,67 @@ const Index = () => {
keywords: question?.tags.map((_) => _.slug_name).join(','), keywords: question?.tags.map((_) => _.slug_name).join(','),
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5 questionDetailPage"> <Row className="questionDetailPage pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0"> {question?.operation?.level && <Alert data={question.operation} />}
{question?.operation?.level && <Alert data={question.operation} />} {isLoading ? (
{isLoading ? ( <ContentLoader />
<ContentLoader /> ) : (
) : ( <Question
<Question data={question}
data={question} initPage={initPage}
initPage={initPage} hasAnswer={answers.count > 0}
hasAnswer={answers.count > 0} isLogged={isLogged}
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 && ( </Col>
<> <Col className="page-right-side mt-4 mt-xl-0">
<AnswerHead count={answers.count} order={order} /> <CustomSidebar />
{answers?.list?.map((item) => { <RelatedQuestions id={question?.id || ''} />
return ( </Col>
<Answer </Row>
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>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; 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 { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -184,14 +184,10 @@ const Index = () => {
title: t('edit_answer', { keyPrefix: 'page_title' }), title: t('edit_answer', { keyPrefix: 'page_title' }),
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> <div className="pt-4 mb-5 edit-answer-wrap">
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} md={12}> <Row>
<h3 className="mb-4">{t('title')}</h3> <Col className="flex-auto">
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<a <a
href={pathFactory.questionLanding(qid, data?.question.url_title)} href={pathFactory.questionLanding(qid, data?.question.url_title)}
target="_blank" target="_blank"
@ -285,7 +281,7 @@ const Index = () => {
</div> </div>
</Form> </Form>
</Col> </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>
<Card.Header> <Card.Header>
{t('title', { keyPrefix: 'how_to_format' })} {t('title', { keyPrefix: 'how_to_format' })}
@ -299,7 +295,7 @@ const Index = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Container> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import { FC } from 'react'; 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 { useMatch, Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -44,48 +44,46 @@ const Questions: FC = () => {
usePageTags({ title: pageTitle, subtitle: slogan }); usePageTags({ title: pageTitle, subtitle: slogan });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Row className="pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12}> <QuestionList
<QuestionList source="questions"
source="questions" data={listData}
data={listData} isLoading={listLoading}
isLoading={listLoading} />
/> </Col>
</Col> <Col className="page-right-side mt-4 mt-xl-0">
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <CustomSidebar />
<CustomSidebar /> {!loggedUser.username && (
{!loggedUser.username && ( <div className="card mb-4">
<div className="card mb-4"> <div className="card-body">
<div className="card-body"> <h5 className="card-title">
<h5 className="card-title"> {t2('website_welcome', {
{t2('website_welcome', { site_name: siteInfo.name,
site_name: siteInfo.name, })}
})} </h5>
</h5> <p className="card-text">{siteInfo.description}</p>
<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 <Link
to={userCenter.getLoginUrl()} to={userCenter.getSignUpUrl()}
className="btn btn-primary" className="btn btn-link ms-2"
onClick={floppyNavigation.handleRouteLinkClick}> onClick={floppyNavigation.handleRouteLinkClick}>
{t('login', { keyPrefix: 'btns' })} {t('signup', { keyPrefix: 'btns' })}
</Link> </Link>
{loginSetting.allow_new_registrations ? ( ) : null}
<Link
to={userCenter.getSignUpUrl()}
className="btn btn-link ms-2"
onClick={floppyNavigation.handleRouteLinkClick}>
{t('signup', { keyPrefix: 'btns' })}
</Link>
) : null}
</div>
</div> </div>
)} </div>
{loggedUser.access_token && <FollowingTags />} )}
<HotQuestions /> {loggedUser.access_token && <FollowingTags />}
</Col> <HotQuestions />
</Row> </Col>
</Container> </Row>
); );
}; };

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react'; 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 { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -123,121 +123,111 @@ const Index: FC = () => {
title: t('review'), title: t('review'),
}); });
return ( return (
<Container className="pt-2 mt-4 mb-5"> <Row className="pt-4 mb-5">
<Row> <h3 className="mb-4">{t('review')}</h3>
<Col lg={{ span: 7, offset: 1 }}> <Col className="flex-auto">
<h3 className="mb-4">{t('review')}</h3>
</Col>
{!noTasks && ro && ( {!noTasks && ro && (
<> <>
<Col lg={{ span: 7, offset: 1 }}> <Alert variant="secondary">
<Alert variant="secondary"> <Stack className="align-items-start">
<Stack className="align-items-start"> <span className="badge text-bg-secondary mb-2">
<span className="badge text-bg-secondary mb-2"> {editBadge}
{editBadge} </span>
</span> <Link to={itemLink} target="_blank">
<Link to={itemLink} target="_blank"> {itemTitle}
{itemTitle} </Link>
</Link> <p className="mb-0">
<p className="mb-0"> {t('edit_summary')}: {editSummary}
{t('edit_summary')}: {editSummary} </p>
</p> </Stack>
</Stack> <Stack
<Stack direction="horizontal"
direction="horizontal" gap={1}
gap={1} className="align-items-baseline mt-2">
className="align-items-baseline mt-2"> <BaseUserCard data={editor} avatarSize="24" />
<BaseUserCard data={editor} avatarSize="24" /> {editTime && (
{editTime && ( <FormatTime
<FormatTime time={editTime}
time={editTime} className="small text-secondary"
className="small text-secondary" preFix={t('proposed')}
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,
}}
/> />
)} )}
{type === 'answer' && </Stack>
info && </Alert>
reviewInfo && {type === 'question' &&
'content' in reviewInfo && ( info &&
<DiffContent reviewInfo &&
className="mt-2" 'content' in reviewInfo && (
objectType={type} <DiffContent
newData={{ className="mt-2"
original_text: reviewInfo.content, objectType={type}
}} oldData={{
oldData={{ title: info.title,
original_text: info.content, original_text: info.content,
}} tags: info.tags,
/> }}
)} newData={{
{type === 'tag' && info && reviewInfo && ( title: reviewInfo.title,
original_text: reviewInfo.content,
tags: reviewInfo.tags,
}}
/>
)}
{type === 'answer' &&
info &&
reviewInfo &&
'content' in reviewInfo && (
<DiffContent <DiffContent
className="mt-2" className="mt-2"
objectType={type} objectType={type}
newData={{ newData={{
original_text: reviewInfo.original_text, original_text: reviewInfo.content,
}} }}
oldData={{ oldData={{
original_text: info.content, original_text: info.content,
}} }}
opts={{ showTitle: false, showTagUrlSlug: false }}
/> />
)} )}
</Col> {type === 'tag' && info && reviewInfo && (
<Col lg={{ span: 7, offset: 1 }}> <DiffContent
<Stack direction="horizontal" gap={2} className="mt-4"> className="mt-2"
<Button objectType={type}
variant="outline-primary" newData={{
disabled={isLoading} original_text: reviewInfo.original_text,
onClick={handlingApprove}> }}
{t('approve', { keyPrefix: 'btns' })} oldData={{
</Button> original_text: info.content,
<Button }}
variant="outline-primary" opts={{ showTitle: false, showTagUrlSlug: false }}
disabled={isLoading} />
onClick={handlingReject}> )}
{t('reject', { keyPrefix: 'btns' })} <Stack direction="horizontal" gap={2} className="mt-4">
</Button> <Button
<Button variant="outline-primary"
variant="outline-primary" disabled={isLoading}
disabled={isLoading} onClick={handlingApprove}>
onClick={handlingSkip}> {t('approve', { keyPrefix: 'btns' })}
{t('skip', { keyPrefix: 'btns' })} </Button>
</Button> <Button
</Stack> variant="outline-primary"
</Col> disabled={isLoading}
onClick={handlingReject}>
{t('reject', { keyPrefix: 'btns' })}
</Button>
<Button
variant="outline-primary"
disabled={isLoading}
onClick={handlingSkip}>
{t('skip', { keyPrefix: 'btns' })}
</Button>
</Stack>
</> </>
)} )}
{noTasks && ( {noTasks && <Empty>{t('empty')}</Empty>}
<Col lg={{ span: 7, offset: 1 }}> </Col>
<Empty>{t('empty')}</Empty>
</Col> <Col className="page-right-side mt-4 mt-xl-0" />
)} </Row>
</Row>
</Container>
); );
}; };

View File

@ -1,5 +1,4 @@
import React from 'react'; import { Row, Col, ListGroup } from 'react-bootstrap';
import { Container, Row, Col, ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@ -39,36 +38,34 @@ const Index = () => {
title: pageTitle, title: pageTitle,
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Row className="pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12} className="mb-3"> <Head data={extra} />
<Head data={extra} /> <SearchHead sort={order} count={count} />
<SearchHead sort={order} count={count} /> <ListGroup className="rounded-0 mb-5">
<ListGroup className="rounded-0 mb-5"> {isLoading ? (
{isLoading ? ( <ListLoader />
<ListLoader /> ) : (
) : ( list?.map((item) => {
list?.map((item) => { return <SearchItem key={item.object.id} data={item} />;
return <SearchItem key={item.object.id} data={item} />; })
}) )}
)} </ListGroup>
</ListGroup>
{!isLoading && !list?.length && <Empty />} {!isLoading && !list?.length && <Empty />}
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
<Pagination <Pagination
currentPage={Number(page)} currentPage={Number(page)}
pageSize={20} pageSize={20}
totalSize={count} totalSize={count}
/> />
</div> </div>
</Col> </Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <Col className="page-right-side mt-4 mt-xl-0">
<Tips /> <Tips />
</Col> </Col>
</Row> </Row>
</Container>
); );
}; };

View File

@ -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);

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; 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 { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -139,14 +139,10 @@ const Index = () => {
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> <div className="pt-4 mb-5">
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} md={12}> <Row>
<h3 className="mb-4">{t('title')}</h3> <Col className="flex-auto">
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="display_name" className="mb-3"> <Form.Group controlId="display_name" className="mb-3">
<Form.Label>{t('form.fields.display_name.label')}</Form.Label> <Form.Label>{t('form.fields.display_name.label')}</Form.Label>
@ -208,7 +204,7 @@ const Index = () => {
</div> </div>
</Form> </Form>
</Col> </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>
<Card.Header> <Card.Header>
{t('title', { keyPrefix: 'how_to_format' })} {t('title', { keyPrefix: 'how_to_format' })}
@ -222,7 +218,7 @@ const Index = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Container> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap'; import { Row, Col, Button } from 'react-bootstrap';
import { import {
useParams, useParams,
Link, Link,
@ -100,64 +100,62 @@ const Questions: FC = () => {
keywords: keywords.join(','), keywords: keywords.join(','),
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Row className="pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12}> {isLoading || listLoading ? (
{isLoading || listLoading ? ( <div className="tag-box mb-5 placeholder-glow">
<div className="tag-box mb-5 placeholder-glow"> <div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} /> <p
<p className="placeholder w-100 d-block align-top"
className="placeholder w-100 d-block align-top" style={{ height: '24px' }}
style={{ height: '24px' }} />
/>
<div <div
className="placeholder d-block align-top" className="placeholder d-block align-top"
style={{ height: '38px', width: '100px' }} 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>
) : ( </div>
<div className="tag-box mb-5"> )}
<h3 className="mb-3"> <QuestionList source="tag" data={listData} isLoading={listLoading} />
<Link </Col>
to={pathFactory.tagLanding(tagInfo.slug_name)} <Col className="page-right-side mt-4 mt-xl-0">
replace <CustomSidebar />
className="link-dark"> <FollowingTags />
{tagInfo.display_name} <HotQuestions />
</Link> </Col>
</h3> </Row>
<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>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; 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 { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -188,14 +188,10 @@ const Index = () => {
title: t('edit_tag', { keyPrefix: 'page_title' }), title: t('edit_tag', { keyPrefix: 'page_title' }),
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> <div className="pt-4 mb-5">
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} md={12}> <Row>
<h3 className="mb-4">{t('title')}</h3> <Col className="flex-auto">
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3"> <Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label> <Form.Label>{t('form.fields.revision.label')}</Form.Label>
@ -291,7 +287,7 @@ const Index = () => {
</div> </div>
</Form> </Form>
</Col> </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>
<Card.Header> <Card.Header>
{t('title', { keyPrefix: 'how_to_format' })} {t('title', { keyPrefix: 'how_to_format' })}
@ -305,7 +301,7 @@ const Index = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Container> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { useParams, useNavigate, Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -132,132 +132,130 @@ const TagIntroduction = () => {
}; };
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Row className="pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12}> <h3 className="mb-3">
<h3 className="mb-3"> <Link
<Link to={pathFactory.tagLanding(tagInfo.slug_name)}
to={pathFactory.tagLanding(tagInfo.slug_name)} replace
replace className="link-dark">
className="link-dark"> {tagInfo.display_name}
{tagInfo.display_name} </Link>
</Link> </h3>
</h3>
<div className="text-secondary mb-4 fs-14"> <div className="text-secondary mb-4 fs-14">
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} /> <FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
<FormatTime <FormatTime
preFix={t('edited_at')} preFix={t('edited_at')}
className="ms-3" className="ms-3"
time={tagInfo.updated_at} time={tagInfo.updated_at}
/>
</div>
<div
className="content text-break fmt"
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
/> />
<div className="mt-4"> </div>
{tagInfo?.member_actions.map((action, index) => {
return ( <div
<Button className="content text-break fmt"
key={action.name} dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
variant="link" />
className={classNames( <div className="mt-4">
'link-secondary btn-no-border p-0 fs-14', {tagInfo?.member_actions.map((action, index) => {
index > 0 && 'ms-3', return (
)} <Button
onClick={() => onAction(action)}> key={action.name}
{action.name} variant="link"
</Button>
);
})}
{isLogged && (
<Link
to={`/tags/${tagInfo?.tag_id}/timeline`}
className={classNames( className={classNames(
'link-secondary btn-no-border p-0 fs-14', 'link-secondary btn-no-border p-0 fs-14',
tagInfo?.member_actions?.length > 0 && 'ms-3', index > 0 && 'ms-3',
)}> )}
{t('history')} onClick={() => onAction(action)}>
</Link> {action.name}
)} </Button>
</div> );
</Col> })}
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> {isLogged && (
<Card> <Link
<Card.Header className="d-flex justify-content-between"> to={`/tags/${tagInfo?.tag_id}/timeline`}
<span>{t('synonyms.title')}</span> className={classNames(
{isEdit ? ( 'link-secondary btn-no-border p-0 fs-14',
<Button tagInfo?.member_actions?.length > 0 && 'ms-3',
variant="link" )}>
className="p-0 btn-no-border" {t('history')}
onClick={handleSave}> </Link>
{t('synonyms.btn_save')} )}
</Button> </div>
) : synonymsData?.member_actions?.find( </Col>
(v) => v.action === 'edit', <Col className="page-right-side mt-4 mt-xl-0">
) ? ( <Card>
<Button <Card.Header className="d-flex justify-content-between">
variant="link" <span>{t('synonyms.title')}</span>
className="p-0 btn-no-border" {isEdit ? (
onClick={handleEdit}> <Button
{t('synonyms.btn_edit')} variant="link"
</Button> className="p-0 btn-no-border"
) : null} onClick={handleSave}>
</Card.Header> {t('synonyms.btn_save')}
<Card.Body> </Button>
{isEdit && ( ) : synonymsData?.member_actions?.find(
<> (v) => v.action === 'edit',
<div className="mb-3"> ) ? (
{t('synonyms.text')}{' '} <Button
<Tag variant="link"
data={{ className="p-0 btn-no-border"
slug_name: tagName || '', onClick={handleEdit}>
main_tag_slug_name: '', {t('synonyms.btn_edit')}
display_name: </Button>
tagInfo?.display_name || tagInfo?.slug_name || '', ) : null}
recommend: false, </Card.Header>
reserved: false, <Card.Body>
}} {isEdit && (
/> <>
</div> <div className="mb-3">
<TagSelector {t('synonyms.text')}{' '}
value={synonymsData?.synonyms} <Tag
onChange={handleTagsChange} data={{
hiddenDescription 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 && </Card.Body>
(synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? ( </Card>
<div className="m-n1"> </Col>
{synonymsData.synonyms.map((item) => { </Row>
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>
); );
}; };

View File

@ -1,13 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { import { Row, Col, Card, Button, Form, Stack } from 'react-bootstrap';
Container,
Row,
Col,
Card,
Button,
Form,
Stack,
} from 'react-bootstrap';
import { useSearchParams, Link } from 'react-router-dom'; import { useSearchParams, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -60,90 +52,89 @@ const Tags = () => {
title: t('tags', { keyPrefix: 'page_title' }), title: t('tags', { keyPrefix: 'page_title' }),
}); });
return ( return (
<Container className="py-3 my-3"> <Row className="py-4 mb-4">
<Row className="mb-4 d-flex justify-content-center"> <Col xxl={12}>
<Col xxl={10} sm={12}> <h3 className="mb-4">{t('title')}</h3>
<h3 className="mb-4">{t('title')}</h3> <div className="d-block d-sm-flex justify-content-between align-items-center flex-wrap">
<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">
<Stack direction="horizontal" gap={3} className="mb-3 mb-sm-0"> <Form>
<Form> <Form.Group controlId="formBasicEmail">
<Form.Group controlId="formBasicEmail"> <Form.Control
<Form.Control value={searchTag}
value={searchTag} placeholder={t('search_placeholder')}
placeholder={t('search_placeholder')} type="text"
type="text" onChange={handleChange}
onChange={handleChange} size="sm"
size="sm" />
/> </Form.Group>
</Form.Group> </Form>
</Form> {role_id === 2 || role_id === 3 ? (
{role_id === 2 || role_id === 3 ? ( <Link
<Link className="btn btn-outline-primary btn-sm"
className="btn btn-outline-primary btn-sm" to="/tags/create">
to="/tags/create"> {t('title', { keyPrefix: 'tag_modal' })}
{t('title', { keyPrefix: 'tag_modal' })} </Link>
</Link> ) : null}
) : null} </Stack>
</Stack> <QueryGroup
<QueryGroup data={sortBtns}
data={sortBtns} currentSort={sort || 'popular'}
currentSort={sort || 'popular'} sortKey="sort"
sortKey="sort" i18nKeyPrefix="tags.sort_buttons"
i18nKeyPrefix="tags.sort_buttons" />
/> </div>
</div> </Col>
</Col>
<Col className="mt-4" xxl={10} sm={12}> <Col className="mt-4" xxl={12}>
<Row> <Row>
{isLoading ? ( {isLoading ? (
<TagsLoader /> <TagsLoader />
) : ( ) : (
tags?.list?.map((tag) => ( tags?.list?.map((tag) => (
<Col <Col
key={tag.slug_name} key={tag.slug_name}
xs={12} xl={3}
lg={3} lg={4}
md={4} md={4}
sm={6} sm={6}
className="mb-4"> xs={12}
<Card className="h-100"> className="mb-4">
<Card.Body className="d-flex flex-column align-items-start"> <Card className="h-100">
<Tag className="mb-3" data={tag} /> <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"> <div className="fs-14 flex-fill text-break text-wrap text-truncate-3 reset-p mb-3">
{escapeRemove(tag.excerpt)} {escapeRemove(tag.excerpt)}
</div> </div>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Button <Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`} className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary" variant="outline-primary"
size="sm" size="sm"
onClick={() => handleFollow(tag)}> onClick={() => handleFollow(tag)}>
{tag.is_follower {tag.is_follower
? t('button_following') ? t('button_following')
: t('button_follow')} : t('button_follow')}
</Button> </Button>
<span className="text-secondary fs-14 text-nowrap"> <span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')} {formatCount(tag.question_count)} {t('tag_label')}
</span> </span>
</div> </div>
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
)) ))
)} )}
</Row> </Row>
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
<Pagination <Pagination
currentPage={page} currentPage={page}
totalSize={tags?.count || 0} totalSize={tags?.count || 0}
pageSize={pageSize} pageSize={pageSize}
/> />
</div> </div>
</Col> </Col>
</Row> </Row>
</Container>
); );
}; };

View File

@ -1,5 +1,5 @@
import { FC, useState, useEffect } from 'react'; 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 { Link, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -79,54 +79,50 @@ const Index: FC = () => {
title: pageTitle, title: pageTitle,
}); });
return ( return (
<Container className="py-3"> <div className="py-4 mb-5">
<Row className="py-3 justify-content-center"> <h5 className="mb-4">
<Col xxl={10}> {timelineData?.object_info.object_type === 'tag'
<h5 className="mb-4"> ? t('tag_title')
{timelineData?.object_info.object_type === 'tag' : t('title')}{' '}
? t('tag_title') <Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
: t('title')}{' '} </h5>
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link> {timelineData?.object_info.object_type !== 'tag' && (
</h5> <Form.Check
{timelineData?.object_info.object_type !== 'tag' && ( className="mb-4"
<Form.Check type="switch"
className="mb-4" id="custom-switch"
type="switch" label={t('show_votes')}
id="custom-switch" checked={showVotes}
label={t('show_votes')} onChange={(e) => handleSwitch(e.target.checked)}
checked={showVotes} />
onChange={(e) => handleSwitch(e.target.checked)} )}
/> <Table hover>
)} <thead>
<Table hover> <tr>
<thead> <th style={{ width: '20%' }}>{t('datetime')}</th>
<tr> <th style={{ width: '15%' }}>{t('type')}</th>
<th style={{ width: '20%' }}>{t('datetime')}</th> <th style={{ width: '19%' }}>{t('by')}</th>
<th style={{ width: '15%' }}>{t('type')}</th> <th>{t('comment')}</th>
<th style={{ width: '19%' }}>{t('by')}</th> </tr>
<th>{t('comment')}</th> </thead>
</tr> <tbody>
</thead> {timelineData?.timeline?.map((item) => {
<tbody> return (
{timelineData?.timeline?.map((item) => { <HistoryItem
return ( data={item}
<HistoryItem objectInfo={timelineData?.object_info}
data={item} key={item.activity_id}
objectInfo={timelineData?.object_info} isAdmin={role_id === 2}
key={item.activity_id} revisionList={revisionList}
isAdmin={role_id === 2} />
revisionList={revisionList} );
/> })}
); </tbody>
})} </Table>
</tbody> {!isLoading && Number(timelineData?.timeline?.length) <= 0 && (
</Table> <Empty>{t('no_data')}</Empty>
{!isLoading && Number(timelineData?.timeline?.length) <= 0 && ( )}
<Empty>{t('no_data')}</Empty> </div>
)}
</Col>
</Row>
</Container>
); );
}; };

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
@ -74,62 +74,60 @@ const Notifications = () => {
title: t('notifications', { keyPrefix: 'page_title' }), title: t('notifications', { keyPrefix: 'page_title' }),
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Row className="pt-4 mb-5">
<Row className="justify-content-center"> <Col className="flex-auto">
<Col xxl={7} lg={8} sm={12}> <h3 className="mb-4">{t('title')}</h3>
<h3 className="mb-4">{t('title')}</h3> <div className="d-flex justify-content-between mb-3">
<div className="d-flex justify-content-between mb-3"> <ButtonGroup size="sm">
<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>
<Button <Button
size="sm" as="a"
href="/users/notifications/inbox"
variant="outline-secondary" variant="outline-secondary"
onClick={handleUnreadNotification}> active={type === 'inbox'}
{t('all_read')} 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> </Button>
</div> </div>
{type === 'inbox' && ( )}
<Inbox </Col>
data={notificationData} <Col className="page-right-side" />
handleReadNotification={handleReadNotification} </Row>
/>
)}
{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>
); );
}; };

View File

@ -1,5 +1,5 @@
import { FC } from 'react'; import { FC } from 'react';
import { Container, Row, Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams, Link } from 'react-router-dom'; import { useParams, useSearchParams, Link } from 'react-router-dom';
@ -58,19 +58,19 @@ const Personal: FC = () => {
}); });
return ( return (
<Container className="pt-4 mt-2 mb-5"> <div className="pt-4 mb-5">
<Row className="justify-content-center"> <Row>
{userInfo?.status !== 'normal' && userInfo?.status_msg && ( {userInfo?.status !== 'normal' && userInfo?.status_msg && (
<Alert data={userInfo?.status_msg} /> <Alert data={userInfo?.status_msg} />
)} )}
<Col xxl={7} lg={8} sm={12}> <Col className="flex-auto">
<UserInfo data={userInfo as UserInfoRes} /> <UserInfo data={userInfo as UserInfoRes} />
</Col> </Col>
<Col <Col
xxl={3} xxl={3}
lg={4} lg={4}
sm={12} 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 && ( {isSelf && (
<div className="mb-3"> <div className="mb-3">
<Link <Link
@ -83,11 +83,9 @@ const Personal: FC = () => {
</Col> </Col>
</Row> </Row>
<Row className="justify-content-center"> <Row>
<Col xxl={10}> <NavBar tabName={tabName} slug={username} isSelf={isSelf} />
<NavBar tabName={tabName} slug={username} isSelf={isSelf} /> <Col className="flex-auto">
</Col>
<Col xxl={7} lg={8} sm={12}>
<Overview <Overview
visible={tabName === 'overview'} visible={tabName === 'overview'}
introduction={userInfo?.bio_html || ''} introduction={userInfo?.bio_html || ''}
@ -120,7 +118,7 @@ const Personal: FC = () => {
</div> </div>
)} )}
</Col> </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> <h5 className="mb-3">{t('stats')}</h5>
{userInfo?.created_at && ( {userInfo?.created_at && (
<> <>
@ -137,7 +135,7 @@ const Personal: FC = () => {
)} )}
</Col> </Col>
</Row> </Row>
</Container> </div>
); );
}; };
export default Personal; export default Personal;

View File

@ -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%;
}
}

View File

@ -1,5 +1,5 @@
import { FC, memo } from 'react'; import { FC, memo } from 'react';
import { Container, Row, Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
@ -7,6 +7,8 @@ import { usePageTags } from '@/hooks';
import Nav from './components/Nav'; import Nav from './components/Nav';
import './index.scss';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'settings.profile', keyPrefix: 'settings.profile',
@ -16,23 +18,14 @@ const Index: FC = () => {
title: t('settings', { keyPrefix: 'page_title' }), title: t('settings', { keyPrefix: 'page_title' }),
}); });
return ( return (
<Container className="mt-4 mb-5 pb-5"> <Row className="mt-4 mb-5 pb-5">
<Row className="justify-content-center"> <Col className="settings-nav mb-4">
<Col xxl={10} md={12}> <Nav />
<h3 className="mb-4">{t('page_title', { keyPrefix: 'settings' })}</h3> </Col>
</Col> <Col className="settings-main">
</Row> <Outlet />
</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>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Container, Row, Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Fragment } from 'react'; import { Fragment } from 'react';
@ -22,61 +22,60 @@ const Users = () => {
const keys = Object.keys(users); const keys = Object.keys(users);
return ( return (
<Container className="py-3 my-3"> <Row className="py-4 mb-4 d-flex justify-content-center">
<Row className="mb-4 d-flex justify-content-center"> <Col xxl={12}>
<Col xxl={10} sm={12}> <h3 className="mb-4">{t('title')}</h3>
<h3 className="mb-4">{t('title')}</h3> </Col>
</Col>
<Col xxl={10} sm={12}> <Col xxl={12}>
{keys.map((key, index) => { {keys.map((key, index) => {
if (users[key]?.length === 0) { if (users[key]?.length === 0) {
return null; return null;
} }
return ( return (
<Fragment key={key}> <Fragment key={key}>
<Row className="mb-4"> <Row className="mb-4">
<Col> <Col>
<h6 className="mb-0">{t(key)}</h6> <h6 className="mb-0">{t(key)}</h6>
</Col> </Col>
</Row> </Row>
<Row className={index === keys.length - 1 ? '' : 'mb-4'}> <Row className={index === keys.length - 1 ? '' : 'mb-4'}>
{users[key]?.map((user) => ( {users[key]?.map((user) => (
<Col <Col
key={user.username} key={user.username}
xs={12} xl={3}
lg={3} lg={4}
md={4} md={4}
sm={6} sm={6}
className="mb-4"> xs={12}
<div className="d-flex"> 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}`}> <Link to={`/users/${user.username}`}>
<Avatar {user.display_name}
size="48px"
avatar={user?.avatar}
searchStr="s=96"
/>
</Link> </Link>
<div className="ms-2"> <div className="text-secondary fs-14">
<Link to={`/users/${user.username}`}> {key === 'users_with_the_most_vote'
{user.display_name} ? `${user.vote_count} ${t('votes')}`
</Link> : `${user.rank} ${t('reputation')}`}
<div className="text-secondary fs-14">
{key === 'users_with_the_most_vote'
? `${user.vote_count} ${t('votes')}`
: `${user.rank} ${t('reputation')}`}
</div>
</div> </div>
</div> </div>
</Col> </div>
))} </Col>
</Row> ))}
</Fragment> </Row>
); </Fragment>
})} );
</Col> })}
</Row> </Col>
</Container> </Row>
); );
}; };

View File

@ -40,127 +40,161 @@ const routes: RouteNode[] = [
children: [ children: [
// question and answer // question and answer
{ {
index: true, // side nav layout
page: 'pages/Questions', path: '/',
}, page: 'pages/SideNavLayout',
{
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();
},
children: [ children: [
{ {
index: true, index: true,
page: 'pages/Users/Settings/Profile', page: 'pages/Questions',
}, },
{ {
path: 'profile', path: 'questions',
page: 'pages/Users/Settings/Profile', page: 'pages/Questions',
}, },
{ {
path: 'notify', path: 'questions/ask',
page: 'pages/Users/Settings/Notification', page: 'pages/Questions/Ask',
guard: () => {
return guard.activated();
},
}, },
{ {
path: 'account', path: 'posts/:qid/edit',
page: 'pages/Users/Settings/Account', page: 'pages/Questions/Ask',
guard: () => {
return guard.activated();
},
}, },
{ {
path: 'interface', path: 'posts/:qid/:aid/edit',
page: 'pages/Users/Settings/Interface', 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', path: 'users/login',
page: 'pages/Users/Login', page: 'pages/Users/Login',
@ -243,27 +277,6 @@ const routes: RouteNode[] = [
path: '/users/auth-landing', path: '/users/auth-landing',
page: 'pages/Users/AuthCallback', 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 // for admin
{ {
path: 'admin', path: 'admin',
@ -374,11 +387,6 @@ const routes: RouteNode[] = [
path: '/user-center/auth-failed', path: '/user-center/auth-failed',
page: 'pages/UserCenter/AuthFailed', page: 'pages/UserCenter/AuthFailed',
}, },
// for review
{
path: 'review',
page: 'pages/Review',
},
{ {
path: '*', path: '*',
page: 'pages/404', page: 'pages/404',

View File

@ -11,6 +11,7 @@ import customizeStore from './customize';
import themeSettingStore from './themeSetting'; import themeSettingStore from './themeSetting';
import loginToContinueStore from './loginToContinue'; import loginToContinueStore from './loginToContinue';
import errorCodeStore from './errorCode'; import errorCodeStore from './errorCode';
import sideNavStore from './sideNav';
export { export {
toastStore, toastStore,
@ -26,4 +27,5 @@ export {
loginToContinueStore, loginToContinueStore,
errorCodeStore, errorCodeStore,
userCenterStore, userCenterStore,
sideNavStore,
}; };

32
ui/src/stores/sideNav.ts Normal file
View File

@ -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;