mirror of https://gitee.com/answerdev/answer.git
Merge branch 'ui' of git.backyard.segmentfault.com:opensource/answer into ui
This commit is contained in:
commit
541b213b1a
|
@ -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>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -73,7 +73,7 @@ const Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const text = `@${item?.displayName}[${item?.userName}] `;
|
||||
const text = `@${item?.userName}`;
|
||||
onSelected(
|
||||
`${value.substring(
|
||||
0,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -675,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",
|
||||
|
@ -857,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -78,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) {
|
||||
|
@ -88,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],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -100,8 +99,8 @@ 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 {
|
||||
|
|
Loading…
Reference in New Issue