mirror of https://gitee.com/answerdev/answer.git
Merge branch 'ui' into 'main'
Api Update See merge request opensource/answer!66
This commit is contained in:
commit
9aa5eb4086
|
@ -195,7 +195,7 @@ export interface PostAnswerReq {
|
|||
}
|
||||
|
||||
export interface PageUser {
|
||||
id;
|
||||
id?;
|
||||
displayName;
|
||||
userName?;
|
||||
avatar_url?;
|
||||
|
|
|
@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import { Avatar } from '@answer/components';
|
||||
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
showAvatar?: boolean;
|
||||
|
@ -34,7 +36,9 @@ const Index: FC<Props> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
<span className="fw-bold">{data?.rank}</span>
|
||||
<span className="fw-bold" title="Reputation">
|
||||
{formatCount(data?.rank)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Button, Dropdown } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
@ -64,6 +64,29 @@ const ActionBar = ({
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<Dropdown className="d-block d-md-none">
|
||||
<Dropdown.Toggle
|
||||
as="div"
|
||||
variant="success"
|
||||
className="no-toggle"
|
||||
id="dropdown-comment">
|
||||
<Icon name="three-dots" className="text-secondary" />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu align="end">
|
||||
{memberActions.map((action) => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={action.name}
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => onAction(action)}>
|
||||
{action.name}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -29,27 +29,33 @@ const Form = ({
|
|||
const handleChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleSelected = (val) => {
|
||||
setValue(val);
|
||||
};
|
||||
return (
|
||||
<div className={classNames('d-flex align-items-start', className)}>
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex align-items-start flex-column flex-md-row',
|
||||
className,
|
||||
)}>
|
||||
<div>
|
||||
<Mentions pageUsers={pageUsers.getUsers()}>
|
||||
<Mentions pageUsers={pageUsers.getUsers()} onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
</div>
|
||||
{type === 'edit' ? (
|
||||
<div className="d-flex flex-column">
|
||||
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap ms-2"
|
||||
className="text-nowrap "
|
||||
onClick={() => onSendReply(value)}>
|
||||
{t('btn_save_edits')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-nowrap ms-2 btn-no-border"
|
||||
className="text-nowrap btn-no-border ms-2 ms-md-0"
|
||||
onClick={onCancel}>
|
||||
{t('btn_cancel')}
|
||||
</Button>
|
||||
|
@ -57,7 +63,7 @@ const Form = ({
|
|||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap ms-2"
|
||||
className="text-nowrap ms-0 ms-md-2 mt-2 mt-md-0"
|
||||
onClick={() => onSendReply(value)}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
|
|
|
@ -13,28 +13,33 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
|||
const handleChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const handleSelected = (val) => {
|
||||
setValue(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="fs-14 mb-2">Reply to {userName}</div>
|
||||
<div className="d-flex mb-1 align-items-start">
|
||||
<div className="d-flex mb-1 align-items-start flex-column flex-md-row">
|
||||
<div>
|
||||
<Mentions pageUsers={pageUsers.getUsers()}>
|
||||
<Mentions
|
||||
pageUsers={pageUsers.getUsers()}
|
||||
onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column">
|
||||
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap ms-2"
|
||||
className="text-nowrap"
|
||||
onClick={() => onSendReply(value)}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-nowrap ms-2 btn-no-border"
|
||||
className="text-nowrap btn-no-border ms-2 ms-md-0"
|
||||
onClick={onCancel}>
|
||||
{t('btn_cancel')}
|
||||
</Button>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
@import 'bootstrap/scss/functions';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'bootstrap/scss/mixins/_breakpoints';
|
||||
|
||||
.comments-wrap {
|
||||
.comment-item {
|
||||
&:hover {
|
||||
.control-area {
|
||||
display: flex !important;
|
||||
@include media-breakpoint-up(md) {
|
||||
.control-area {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { Nav, Dropdown } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
||||
import { Avatar, Icon } from '@answer/components';
|
||||
|
||||
interface Props {
|
||||
redDot;
|
||||
userInfo;
|
||||
logOut: () => void;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Nav.Link
|
||||
as={NavLink}
|
||||
to="/users/notifications/inbox"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<div className="text-white text-opacity-75">
|
||||
<Icon name="bell-fill" className="fs-4" />
|
||||
</div>
|
||||
{(redDot?.inbox || 0) > 0 && <div className="unread-dot bg-danger" />}
|
||||
</Nav.Link>
|
||||
|
||||
<Nav.Link
|
||||
as={Link}
|
||||
to="/users/notifications/achievement"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<div className="text-white text-opacity-75">
|
||||
<Icon name="trophy-fill" className="fs-4" />
|
||||
</div>
|
||||
{(redDot?.achievement || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger" />
|
||||
)}
|
||||
</Nav.Link>
|
||||
|
||||
<Dropdown align="end">
|
||||
<Dropdown.Toggle
|
||||
variant="success"
|
||||
id="dropdown-basic"
|
||||
as="a"
|
||||
className="no-toggle pointer">
|
||||
<Avatar size="36px" avatar={userInfo?.avatar} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href={`/users/${userInfo.username}`}>
|
||||
{t('header.nav.profile')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href="/users/settings/profile">
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{userInfo?.is_admin ? (
|
||||
<Dropdown.Item href="/admin">{t('header.nav.admin')}</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={logOut}>
|
||||
{t('header.nav.logout')}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -14,8 +14,8 @@
|
|||
color: #fff;
|
||||
}
|
||||
&.icon-link {
|
||||
width: 46px;
|
||||
height: 38px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
.placeholder-search {
|
||||
|
@ -28,4 +28,40 @@
|
|||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.answer-navBar {
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
.answer-navBar:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.lg-none {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.hr {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 992.9px) {
|
||||
#header {
|
||||
.nav-grow {
|
||||
flex-grow: 1!important;
|
||||
}
|
||||
|
||||
.lg-none {
|
||||
display: flex!important;
|
||||
}
|
||||
|
||||
.w-75 {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -7,16 +7,22 @@ import {
|
|||
FormControl,
|
||||
Button,
|
||||
Col,
|
||||
Dropdown,
|
||||
} from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, NavLink, Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useSearchParams,
|
||||
NavLink,
|
||||
Link,
|
||||
useNavigate,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { Avatar, Icon } from '@answer/components';
|
||||
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@answer/api';
|
||||
import Storage from '@answer/utils/storage';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Header: FC = () => {
|
||||
|
@ -29,6 +35,7 @@ const Header: FC = () => {
|
|||
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
||||
const { interface: interfaceInfo } = interfaceStore();
|
||||
const { data: redDot } = useQueryNotificationStatus();
|
||||
const location = useLocation();
|
||||
const handleInput = (val) => {
|
||||
setSearch(val);
|
||||
};
|
||||
|
@ -45,18 +52,61 @@ const Header: FC = () => {
|
|||
handleInput(q);
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
useEffect(() => {
|
||||
const collapse = document.querySelector('#navBarContent');
|
||||
if (collapse && collapse.classList.contains('show')) {
|
||||
const toogle = document.querySelector('#navBarToggle') as HTMLElement;
|
||||
if (toogle) {
|
||||
toogle?.click();
|
||||
}
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<Navbar variant="dark" expand="lg" className="sticky-top" id="header">
|
||||
<Container className="d-flex align-items-center">
|
||||
<Navbar.Brand className="lh-1" href="/">
|
||||
{interfaceInfo.logo ? (
|
||||
<img className="logo" src={interfaceInfo.logo} alt="" />
|
||||
) : (
|
||||
<span>{siteInfo.name || 'Answer'}</span>
|
||||
)}
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="navBarContent" />
|
||||
<Navbar.Toggle
|
||||
aria-controls="navBarContent"
|
||||
className="answer-navBar me-2"
|
||||
id="navBarToggle"
|
||||
/>
|
||||
|
||||
<div className="left-wrap d-flex justify-content-between align-items-center nav-grow">
|
||||
<Navbar.Brand to="/" as={Link} className="lh-1">
|
||||
{interfaceInfo.logo ? (
|
||||
<img
|
||||
className="logo rounded-1 me-0"
|
||||
src={interfaceInfo.logo}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<span>{siteInfo.name || 'Answer'}</span>
|
||||
)}
|
||||
</Navbar.Brand>
|
||||
|
||||
{/* mobile nav */}
|
||||
<div className="d-flex lg-none align-items-center flex-lg-nowrap">
|
||||
{user?.username ? (
|
||||
<NavItems redDot={redDot} userInfo={user} logOut={handleLogout} />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
className="me-2 text-white"
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
<Button variant="light" href="/users/register">
|
||||
{t('btns.signup')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</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">
|
||||
|
@ -70,9 +120,10 @@ const Header: FC = () => {
|
|||
</NavLink>
|
||||
</Nav>
|
||||
</Col>
|
||||
<hr className="hr lg-none mt-2" />
|
||||
|
||||
<Col md={4} className="d-none d-sm-flex justify-content-center">
|
||||
<Form action="/search" className="w-75 px-2">
|
||||
<Col lg={4} className="d-flex justify-content-center">
|
||||
<Form action="/search" className="w-75 px-0 px-lg-2">
|
||||
<FormControl
|
||||
placeholder={t('header.search.placeholder')}
|
||||
className="text-white placeholder-search"
|
||||
|
@ -83,69 +134,32 @@ const Header: FC = () => {
|
|||
</Form>
|
||||
</Col>
|
||||
|
||||
<Nav.Item className="lg-none mt-3 pb-1">
|
||||
<Link
|
||||
to="/questions/ask"
|
||||
className="text-capitalize text-nowrap btn btn-light">
|
||||
{t('btns.add_question')}
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
{/* pc nav */}
|
||||
<Col
|
||||
md={4}
|
||||
className="d-flex justify-content-start justify-content-sm-end">
|
||||
lg={4}
|
||||
className="d-none d-lg-flex justify-content-start justify-content-sm-end">
|
||||
{user?.username ? (
|
||||
<Nav className="d-flex align-items-center flex-lg-nowrap">
|
||||
<Nav.Item className="me-2">
|
||||
<Nav.Item className="me-3">
|
||||
<Link
|
||||
to="/questions/ask"
|
||||
className="text-capitalize text-nowrap btn btn-light">
|
||||
{t('btns.add_question')}
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Link
|
||||
as={NavLink}
|
||||
to="/users/notifications/inbox"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
|
||||
<div className="text-white text-opacity-75">
|
||||
<Icon name="bell-fill" className="fs-5" />
|
||||
</div>
|
||||
{(redDot?.inbox || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger" />
|
||||
)}
|
||||
</Nav.Link>
|
||||
|
||||
<Nav.Link
|
||||
as={Link}
|
||||
to="/users/notifications/achievement"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
|
||||
<div className="text-white text-opacity-75">
|
||||
<Icon name="trophy-fill" className="fs-5" />
|
||||
</div>
|
||||
{(redDot?.achievement || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger" />
|
||||
)}
|
||||
</Nav.Link>
|
||||
|
||||
<Dropdown align="end">
|
||||
<Dropdown.Toggle
|
||||
variant="success"
|
||||
id="dropdown-basic"
|
||||
as="a"
|
||||
className="no-toggle pointer">
|
||||
<Avatar size="36px" avatar={user?.avatar} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href={`/users/${user.username}`}>
|
||||
{t('header.nav.profile')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href="/users/settings/profile">
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{user?.is_admin ? (
|
||||
<Dropdown.Item href="/admin">
|
||||
{t('header.nav.admin')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={handleLogout}>
|
||||
{t('header.nav.logout')}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<NavItems
|
||||
redDot={redDot}
|
||||
userInfo={user}
|
||||
logOut={handleLogout}
|
||||
/>
|
||||
</Nav>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -6,11 +6,12 @@ import * as Types from '@answer/common/interface';
|
|||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
pageUsers;
|
||||
onSelected: (val: string) => void;
|
||||
}
|
||||
|
||||
const MAX_RECODE = 5;
|
||||
|
||||
const Mentions: FC<IProps> = ({ children, pageUsers }) => {
|
||||
const Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [val, setValue] = useState('');
|
||||
|
@ -71,23 +72,17 @@ const Mentions: FC<IProps> = ({ children, pageUsers }) => {
|
|||
if (!selectionStart) {
|
||||
return;
|
||||
}
|
||||
const str = value.substring(
|
||||
value.substring(0, selectionStart).lastIndexOf('@'),
|
||||
selectionStart,
|
||||
|
||||
const text = `@${item?.userName}`;
|
||||
onSelected(
|
||||
`${value.substring(
|
||||
0,
|
||||
value.substring(0, selectionStart).lastIndexOf('@'),
|
||||
)}${text}${value.substring(selectionStart)}`,
|
||||
);
|
||||
const text = `@${item?.displayName}[${item?.userName}] `;
|
||||
element.value = `${value.substring(
|
||||
0,
|
||||
value.substring(0, selectionStart).lastIndexOf('@'),
|
||||
)}${text}${value.substring(selectionStart)}`;
|
||||
setUsers([]);
|
||||
setValue('');
|
||||
const newSelectionStart = selectionStart + text.length - str.length;
|
||||
|
||||
element.setSelectionRange(newSelectionStart, newSelectionStart);
|
||||
element.focus();
|
||||
};
|
||||
|
||||
const filterData = val
|
||||
? users.filter(
|
||||
(item) =>
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ButtonGroup, Button } from 'react-bootstrap';
|
||||
import { ButtonGroup, Button, DropdownButton, Dropdown } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
data: string[] | Array<{ name: string; sort: string }>;
|
||||
data;
|
||||
i18nkeyPrefix: string;
|
||||
currentSort: string;
|
||||
sortKey?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MAX_BUTTON_COUNT = 3;
|
||||
const Index: FC<Props> = ({
|
||||
data,
|
||||
data = [],
|
||||
currentSort = '',
|
||||
sortKey = 'order',
|
||||
i18nkeyPrefix = '',
|
||||
|
@ -37,9 +39,13 @@ const Index: FC<Props> = ({
|
|||
setUrlSearchParams(str);
|
||||
};
|
||||
|
||||
const filteredData = data.filter((_, index) => index > MAX_BUTTON_COUNT - 2);
|
||||
const currentBtn = filteredData.find((btn) => {
|
||||
return (typeof btn === 'string' ? btn : btn.name) === currentSort;
|
||||
});
|
||||
return (
|
||||
<ButtonGroup size="sm">
|
||||
{data.map((btn) => {
|
||||
{data.map((btn, index) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
|
@ -48,13 +54,55 @@ const Index: FC<Props> = ({
|
|||
key={key}
|
||||
variant="outline-secondary"
|
||||
active={currentSort === name}
|
||||
className={`text-capitalize ${className}`}
|
||||
className={classNames(
|
||||
'text-capitalize',
|
||||
data.length > MAX_BUTTON_COUNT &&
|
||||
index > MAX_BUTTON_COUNT - 2 &&
|
||||
'd-none d-md-block',
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
data.length > MAX_BUTTON_COUNT && index === data.length - 1
|
||||
? {
|
||||
borderTopRightRadius: '0.25rem',
|
||||
borderBottomRightRadius: '0.25rem',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
href={handleParams(key)}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{data.length > MAX_BUTTON_COUNT && (
|
||||
<DropdownButton
|
||||
size="sm"
|
||||
variant={currentBtn ? 'secondary' : 'outline-secondary'}
|
||||
className="d-block d-md-none"
|
||||
as={ButtonGroup}
|
||||
title={currentBtn ? t(currentSort) : t('more')}>
|
||||
{filteredData.map((btn) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
key={key}
|
||||
active={currentSort === name}
|
||||
className={classNames(
|
||||
'text-capitalize',
|
||||
'd-block d-md-none',
|
||||
className,
|
||||
)}
|
||||
href={handleParams(key)}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</DropdownButton>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Avatar, FormatTime } from '@answer/components';
|
||||
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
time: number;
|
||||
preFix: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ data, time, preFix }) => {
|
||||
const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className={classnames('d-flex', className)}>
|
||||
{data?.status !== 'deleted' ? (
|
||||
<Link to={`/users/${data?.username}`}>
|
||||
<Avatar avatar={data?.avatar} size="40px" className="me-2" />
|
||||
<Avatar
|
||||
avatar={data?.avatar}
|
||||
size="40px"
|
||||
className="me-2 d-none d-md-block"
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
avatar={data?.avatar}
|
||||
size="24px"
|
||||
className="me-2 d-block d-md-none"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Avatar avatar={data?.avatar} size="40px" className="me-2" />
|
||||
)}
|
||||
<div className="fs-14 text-secondary">
|
||||
<div>
|
||||
<div className="fs-14 text-secondary d-flex flex-row flex-md-column align-items-center align-items-md-start">
|
||||
<div className="me-1 me-md-0">
|
||||
{data?.status !== 'deleted' ? (
|
||||
<Link to={`/users/${data?.username}`} className="me-1 text-break">
|
||||
{data?.display_name}
|
||||
|
@ -28,7 +43,9 @@ const Index: FC<Props> = ({ data, time, preFix }) => {
|
|||
) : (
|
||||
<span className="me-1 text-break">{data?.display_name}</span>
|
||||
)}
|
||||
<span className="fw-bold">{data?.rank}</span>
|
||||
<span className="fw-bold" title="Reputation">
|
||||
{formatCount(data?.rank)}
|
||||
</span>
|
||||
</div>
|
||||
{time && <FormatTime time={time} preFix={preFix} />}
|
||||
</div>
|
||||
|
|
|
@ -14,11 +14,11 @@ const usePageUsers = () => {
|
|||
getUsers,
|
||||
setUsers: (data: Types.PageUser | Types.PageUser[]) => {
|
||||
if (data instanceof Array) {
|
||||
setUsers(uniqBy([...users, ...data], 'name'));
|
||||
globalUsers = uniqBy([...globalUsers, ...data], 'name');
|
||||
setUsers(uniqBy([...users, ...data], 'userName'));
|
||||
globalUsers = uniqBy([...globalUsers, ...data], 'userName');
|
||||
} else {
|
||||
setUsers(uniqBy([...users, data], 'name'));
|
||||
globalUsers = uniqBy([...globalUsers, data], 'name');
|
||||
setUsers(uniqBy([...users, data], 'userName'));
|
||||
globalUsers = uniqBy([...globalUsers, data], 'userName');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -117,9 +117,8 @@ const useReportModal = (callback?: () => void) => {
|
|||
if (params.isBackend && params.action === 'review') {
|
||||
putReport({
|
||||
action: params.type,
|
||||
// FIXME: typo
|
||||
flaged_content: content.value,
|
||||
flaged_type: reportType.type,
|
||||
flagged_content: content.value,
|
||||
flagged_type: reportType.type,
|
||||
id: params.id,
|
||||
}).then(() => {
|
||||
callback?.();
|
||||
|
|
|
@ -495,6 +495,13 @@
|
|||
"msg": "Display name cannot be empty.",
|
||||
"msg_range": "Display name up to 30 characters"
|
||||
},
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"caption": "People can mention you as @username",
|
||||
"msg": "Username cannot be empty.",
|
||||
"msg_range": "Username up to 30 characters",
|
||||
"character": "Must use the character set \"a-z\", \"0-9\", \" - . _\""
|
||||
},
|
||||
"avatar": {
|
||||
"label": "Profile image",
|
||||
"text": "You can upload your image or <1>reset</1> it to"
|
||||
|
@ -668,7 +675,8 @@
|
|||
"answered": "answered",
|
||||
"asked": "asked",
|
||||
"closed": "closed",
|
||||
"follow_a_tag": "Follow a tag"
|
||||
"follow_a_tag": "Follow a tag",
|
||||
"more": "More"
|
||||
},
|
||||
"personal": {
|
||||
"overview": "Overview",
|
||||
|
@ -850,4 +858,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,8 +53,8 @@ a {
|
|||
height: 18px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 22px;
|
||||
top: 3px;
|
||||
left: 20px;
|
||||
top: 0px;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
|
|
|
@ -118,8 +118,8 @@ const Flags: FC = () => {
|
|||
className="fs-14 text-secondary"
|
||||
/>
|
||||
<BaseUserCard data={li.report_user} className="mt-2 mb-2" />
|
||||
{li.flaged_reason ? (
|
||||
<small>{li.flaged_content}</small>
|
||||
{li.flagged_reason ? (
|
||||
<small>{li.flagged_content}</small>
|
||||
) : (
|
||||
<small>
|
||||
{li.reason?.name}
|
||||
|
|
|
@ -412,7 +412,7 @@ const Ask = () => {
|
|||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<Card className="mb-4">
|
||||
<Card.Header>
|
||||
{t('title', { keyPrefix: 'how_to_format' })}
|
||||
|
|
|
@ -102,7 +102,7 @@ const Index: FC<Props> = ({
|
|||
</div>
|
||||
|
||||
<Row className="mt-4 mb-3">
|
||||
<Col>
|
||||
<Col className="mb-3 mb-md-0">
|
||||
<Operate
|
||||
qid={data.question_id}
|
||||
aid={data.id}
|
||||
|
@ -113,7 +113,7 @@ const Index: FC<Props> = ({
|
|||
callback={callback}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<Col lg={3} className="mb-3 mb-md-0">
|
||||
{data.update_user_info?.username !== data.user_info?.username ? (
|
||||
<UserCard
|
||||
data={data?.update_user_info}
|
||||
|
|
|
@ -57,7 +57,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<h1 className="fs-3 mb-3 text-wrap text-break">
|
||||
<h1 className="h3 mb-3 text-wrap text-break">
|
||||
<Link className="link-dark" reloadDocument to={`/questions/${data.id}`}>
|
||||
{data.title}
|
||||
{data.status === 2
|
||||
|
@ -65,7 +65,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
: ''}
|
||||
</Link>
|
||||
</h1>
|
||||
<div className="d-flex align-items-center fs-14 mb-2 text-secondary">
|
||||
|
||||
<div className="d-flex flex-wrap align-items-center fs-14 mb-3 text-secondary">
|
||||
<FormatTime
|
||||
time={data.create_time}
|
||||
preFix={t('Asked')}
|
||||
|
@ -90,7 +91,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
{followed ? 'Following' : 'Follow'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-2 mx-n1">
|
||||
<div className="m-n1">
|
||||
{data?.tags?.map((item: any) => {
|
||||
return (
|
||||
<Tag
|
||||
|
@ -105,7 +106,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
<article
|
||||
ref={ref}
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt text-break text-wrap"
|
||||
className="fmt text-break text-wrap mt-4"
|
||||
/>
|
||||
|
||||
<Actions
|
||||
|
@ -122,7 +123,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
/>
|
||||
|
||||
<Row className="mt-4 mb-3">
|
||||
<Col lg={5}>
|
||||
<Col lg={5} className="mb-3 mb-md-0">
|
||||
<Operate
|
||||
qid={data?.id}
|
||||
type="question"
|
||||
|
@ -133,7 +134,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
callback={initPage}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<Col lg={3} className="mb-3 mb-md-0">
|
||||
{data.update_user_info?.username !== data.user_info?.username ? (
|
||||
<UserCard
|
||||
data={data?.user_info}
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
.answer-item {
|
||||
border-top: 1px solid rgba(33, 37, 41, 0.25);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.questionDetailPage {
|
||||
h1.h3 {
|
||||
font-size: calc(1.275rem + .3vw)!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,16 @@ const Index = () => {
|
|||
}
|
||||
|
||||
res.list.forEach((item) => {
|
||||
setUsers([item.user_info, item?.update_user_info]);
|
||||
setUsers([
|
||||
{
|
||||
displayName: item.user_info.display_name,
|
||||
userName: item.user_info.username,
|
||||
},
|
||||
{
|
||||
displayName: item?.update_user_info?.display_name,
|
||||
userName: item?.update_user_info?.username,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -107,9 +116,9 @@ const Index = () => {
|
|||
return (
|
||||
<>
|
||||
<PageTitle title={question?.title} />
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
|
||||
{question?.operation?.operation_type && (
|
||||
<Alert data={question.operation} />
|
||||
)}
|
||||
|
@ -145,6 +154,7 @@ const Index = () => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!question?.operation?.operation_type && (
|
||||
<WriteAnswer
|
||||
visible={answers.count === 0}
|
||||
|
@ -156,7 +166,7 @@ const Index = () => {
|
|||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<RelatedQuestions id={question?.id || ''} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -217,7 +217,7 @@ const Ask = () => {
|
|||
</div>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
{t('title', { keyPrefix: 'how_to_format' })}
|
||||
|
|
|
@ -29,7 +29,7 @@ const Questions: FC = () => {
|
|||
<Col xxl={7} lg={8} sm={12}>
|
||||
<QuestionList source="questions" />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<FollowingTags />
|
||||
<HotQuestions />
|
||||
</Col>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { QueryGroup } from '@answer/components';
|
||||
|
||||
const sortBtns = ['newest', 'active', 'score'];
|
||||
const sortBtns = ['relevance', 'newest', 'active', 'score'];
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
|
|
|
@ -13,7 +13,7 @@ const Index = () => {
|
|||
const [searchParams] = useSearchParams();
|
||||
const page = searchParams.get('page') || 1;
|
||||
const q = searchParams.get('q') || '';
|
||||
const order = searchParams.get('order') || 'newest';
|
||||
const order = searchParams.get('order') || 'relevance';
|
||||
|
||||
const { data, isLoading } = useSearch({
|
||||
q,
|
||||
|
@ -53,7 +53,7 @@ const Index = () => {
|
|||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<Tips />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -90,7 +90,7 @@ const Questions: FC = () => {
|
|||
</div>
|
||||
<QuestionList source="tag" />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<FollowingTags />
|
||||
<HotQuestions />
|
||||
</Col>
|
||||
|
|
|
@ -249,7 +249,7 @@ const Ask = () => {
|
|||
</div>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
{t('title', { keyPrefix: 'how_to_format' })}
|
||||
|
|
|
@ -93,7 +93,7 @@ const TagIntroduction = () => {
|
|||
<PageTitle title={pageTitle} />
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={7}>
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={`/tags/${tagInfo?.slug_name}`}
|
||||
|
@ -133,7 +133,7 @@ const TagIntroduction = () => {
|
|||
})}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<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>
|
||||
|
|
|
@ -120,7 +120,7 @@ const Notifications = () => {
|
|||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} />
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
|
|
|
@ -60,7 +60,11 @@ const Personal: FC = () => {
|
|||
<Col xxl={7} lg={8} sm={12}>
|
||||
<UserInfo data={userInfo?.info} />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="d-flex justify-content-end">
|
||||
<Col
|
||||
xxl={3}
|
||||
lg={4}
|
||||
sm={12}
|
||||
className="d-flex justify-content-end mt-5 mt-lg-0">
|
||||
{isSelf && (
|
||||
<div>
|
||||
<Button
|
||||
|
@ -111,7 +115,7 @@ const Personal: FC = () => {
|
|||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12}>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<h5 className="mb-3">Stats</h5>
|
||||
{userInfo?.info && (
|
||||
<>
|
||||
|
|
|
@ -22,6 +22,11 @@ const Index: React.FC = () => {
|
|||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
username: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
avatar: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
|
@ -66,7 +71,7 @@ const Index: React.FC = () => {
|
|||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { display_name, website } = formData;
|
||||
const { display_name, website, username } = formData;
|
||||
if (!display_name.value) {
|
||||
bol = false;
|
||||
formData.display_name = {
|
||||
|
@ -83,6 +88,29 @@ const Index: React.FC = () => {
|
|||
};
|
||||
}
|
||||
|
||||
if (!username.value) {
|
||||
bol = false;
|
||||
formData.username = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('username.msg'),
|
||||
};
|
||||
} else if ([...username.value].length > 30) {
|
||||
bol = false;
|
||||
formData.username = {
|
||||
value: username.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('username.msg_range'),
|
||||
};
|
||||
} else if (/[^a-z0-9\-._]/.test(username.value)) {
|
||||
bol = false;
|
||||
formData.username = {
|
||||
value: username.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('username.character'),
|
||||
};
|
||||
}
|
||||
|
||||
const reg = /^(http|https):\/\//g;
|
||||
if (website.value && !website.value.match(reg)) {
|
||||
bol = false;
|
||||
|
@ -101,12 +129,13 @@ const Index: React.FC = () => {
|
|||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (checkValidated() === false) {
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
display_name: formData.display_name.value,
|
||||
username: formData.username.value,
|
||||
avatar: formData.avatar.value,
|
||||
bio: formData.bio.value,
|
||||
website: formData.website.value,
|
||||
|
@ -137,6 +166,7 @@ const Index: React.FC = () => {
|
|||
const getProfile = () => {
|
||||
getUserInfo().then((res) => {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.username.value = res.username;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
formData.location.value = res.location;
|
||||
|
@ -172,6 +202,29 @@ const Index: React.FC = () => {
|
|||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="userName" className="mb-3">
|
||||
<Form.Label>{t('username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.username.value}
|
||||
isInvalid={formData.username.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||
<div className="d-flex align-items-center">
|
||||
|
|
|
@ -9,9 +9,18 @@ function getQueryString(name: string): string {
|
|||
return '';
|
||||
}
|
||||
|
||||
function thousandthDivision(num) {
|
||||
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
||||
return `${num}`.replace(reg, '$&,');
|
||||
}
|
||||
|
||||
function formatCount($num: number): string {
|
||||
let res = String($num);
|
||||
if ($num >= 1000 && $num < 1000000) {
|
||||
if (!Number.isFinite($num)) {
|
||||
res = '0';
|
||||
} else if ($num < 10000) {
|
||||
res = thousandthDivision($num);
|
||||
} else if ($num < 1000000) {
|
||||
res = `${Math.round($num / 100) / 10}k`;
|
||||
} else if ($num >= 1000000) {
|
||||
res = `${Math.round($num / 100000) / 10}m`;
|
||||
|
@ -69,8 +78,8 @@ function scrollTop(element) {
|
|||
* @returns Array<{displayName: string, userName: string}>
|
||||
*/
|
||||
function matchedUsers(markdown) {
|
||||
const globalReg = /@(.*?)\[(.*?)\]/gm;
|
||||
const reg = /@(.*?)\[(.*?)\]/;
|
||||
const globalReg = /\B@([\w|]+)/g;
|
||||
const reg = /\B@([\w\\_\\.]+)/;
|
||||
|
||||
const users = markdown.match(globalReg);
|
||||
if (!users) {
|
||||
|
@ -79,8 +88,7 @@ function matchedUsers(markdown) {
|
|||
return users.map((user) => {
|
||||
const matched = user.match(reg);
|
||||
return {
|
||||
displayName: matched[2],
|
||||
userName: matched[2],
|
||||
userName: matched[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -91,12 +99,13 @@ function matchedUsers(markdown) {
|
|||
* @returns string
|
||||
*/
|
||||
function parseUserInfo(markdown) {
|
||||
const globalReg = /@(.*?)\[(.*?)\]/gm;
|
||||
return markdown.replace(globalReg, '[@$1](/u/$2)');
|
||||
const globalReg = /\B@([\w\\_\\.\\-]+)/g;
|
||||
return markdown.replace(globalReg, '[@$1](/u/$1)');
|
||||
}
|
||||
|
||||
export {
|
||||
getQueryString,
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
isLogin,
|
||||
scrollTop,
|
||||
|
|
Loading…
Reference in New Issue