Merge branch 'ui' of git.backyard.segmentfault.com:opensource/answer into ui

This commit is contained in:
haitao(lj) 2022-10-14 17:12:40 +08:00
commit 541b213b1a
8 changed files with 252 additions and 85 deletions

View File

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

View File

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

View File

@ -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>
) : (
<>

View File

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

View File

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

View File

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

View File

@ -53,8 +53,8 @@ a {
height: 18px;
border-radius: 50%;
position: absolute;
left: 22px;
top: 3px;
left: 20px;
top: 0px;
border: 1px solid #fff;
}

View File

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