fix: resolve conflict

This commit is contained in:
shuai 2022-10-14 16:15:36 +08:00
commit 65eb12f504
23 changed files with 231 additions and 57 deletions

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->

View File

View File

@ -195,7 +195,7 @@ export interface PostAnswerReq {
}
export interface PageUser {
id;
id?;
displayName;
userName?;
avatar_url?;

View File

@ -45,7 +45,7 @@ const Index: FC<Props> = ({ className, data }) => {
count: data?.collectCount,
});
}
}, [data]);
}, []);
const handleVote = (type: 'up' | 'down') => {
if (!isLogin(true)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ const Header: FC = () => {
/>
<div className="left-wrap d-flex justify-content-between align-items-center nav-grow">
<Navbar.Brand to="/" as={Link}>
<Navbar.Brand to="/" as={Link} className="lh-1">
{interfaceInfo.logo ? (
<img
className="logo rounded-1 me-0"

View File

@ -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?.displayName}[${item?.userName}] `;
element.value = `${value.substring(
0,
value.substring(0, selectionStart).lastIndexOf('@'),
)}${text}${value.substring(selectionStart)}`;
onSelected(
`${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) =>

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

@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
import { Avatar, FormatTime } from '@answer/components';
import { formatCount } from '@/utils';
interface Props {
data: any;
time: number;
@ -28,7 +30,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>

View File

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

View File

@ -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?.();

View File

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

View File

@ -14,6 +14,10 @@
}
}
:root {
overflow-y: scroll;
}
html,
body {
padding: 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`;
@ -97,6 +106,7 @@ function parseUserInfo(markdown) {
export {
getQueryString,
thousandthDivision,
formatCount,
isLogin,
scrollTop,