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
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.

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}>
{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')}

View File

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

View File

@ -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')}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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>
);
};

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 { 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>
);
};

View File

@ -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',

View File

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

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;