mirror of https://gitee.com/answerdev/answer.git
commit
16e0c9d12a
|
@ -525,6 +525,7 @@ ui:
|
|||
tip_answer: >-
|
||||
Use comments to reply to other users or notify them of changes. If you are
|
||||
adding new information, edit your post instead of commenting.
|
||||
tip_vote: It adds something useful to the post
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
|
@ -784,6 +785,11 @@ ui:
|
|||
answered: answered
|
||||
closed_in: Closed in
|
||||
show_exist: Show existing question.
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
title: Answers
|
||||
score: Score
|
||||
|
@ -801,10 +807,19 @@ ui:
|
|||
edit link to refine and improve your existing answer, instead.</p>
|
||||
empty: Answer cannot be empty.
|
||||
characters: content must be at least 6 characters in length.
|
||||
tips:
|
||||
header_1: Thanks for your answer
|
||||
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
|
||||
li1_2: Back up any statements you make with references or personal experience.
|
||||
header_2: But <strong>avoid</strong> ...
|
||||
li2_1: Asking for help, seeking clarification, or responding to other answers.
|
||||
|
||||
reopen:
|
||||
confirm_btn: Reopen
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
success: This post has been reopened
|
||||
|
||||
delete:
|
||||
title: Delete this post
|
||||
question: >-
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, FC, useState, useEffect } from 'react';
|
||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||
import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
@ -12,6 +12,7 @@ import { bookmark, postVote } from '@/services';
|
|||
|
||||
interface Props {
|
||||
className?: string;
|
||||
source: 'question' | 'answer';
|
||||
data: {
|
||||
id: string;
|
||||
votesCount: number;
|
||||
|
@ -24,7 +25,7 @@ interface Props {
|
|||
};
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ className, data }) => {
|
||||
const Index: FC<Props> = ({ className, data, source }) => {
|
||||
const [votes, setVotes] = useState(0);
|
||||
const [like, setLike] = useState(false);
|
||||
const [hate, setHated] = useState(false);
|
||||
|
@ -101,21 +102,40 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
return (
|
||||
<div className={classNames(className)}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
active={like}
|
||||
onClick={() => handleVote('up')}>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
</Button>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip>
|
||||
{source === 'question'
|
||||
? t('question_detail.question_useful')
|
||||
: t('question_detail.answer_useful')}
|
||||
</Tooltip>
|
||||
}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
active={like}
|
||||
onClick={() => handleVote('up')}>
|
||||
<Icon name="hand-thumbs-up-fill me-2" />
|
||||
<span>{t('question_detail.useful')}</span>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
<Button variant="outline-secondary" className="opacity-100" disabled>
|
||||
{votes}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
active={hate}
|
||||
onClick={() => handleVote('down')}>
|
||||
<Icon name="hand-thumbs-down-fill" />
|
||||
</Button>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip>
|
||||
{source === 'question'
|
||||
? t('question_detail.question_un_useful')
|
||||
: t('question_detail.answer_un_useful')}
|
||||
</Tooltip>
|
||||
}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
active={hate}
|
||||
onClick={() => handleVote('down')}>
|
||||
<Icon name="hand-thumbs-down-fill" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</ButtonGroup>
|
||||
{!data?.hideCollect && (
|
||||
<Button
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
import { Button, Dropdown } from 'react-bootstrap';
|
||||
import { Button, Dropdown, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
@ -31,14 +31,19 @@ const ActionBar = ({
|
|||
)}
|
||||
<span className="mx-1">•</span>
|
||||
<FormatTime time={createdAt} className="me-3" />
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={`me-3 btn-no-border p-0 ${isVote ? '' : 'link-secondary'}`}
|
||||
onClick={onVote}>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
{voteCount > 0 && <span className="ms-2">{voteCount}</span>}
|
||||
</Button>
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip className="comment-tip">{t('tip_vote')}</Tooltip>}>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={`me-3 btn-no-border p-0 ${
|
||||
isVote ? '' : 'link-secondary'
|
||||
}`}
|
||||
onClick={onVote}>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
{voteCount > 0 && <span className="ms-2">{voteCount}</span>}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
|
|
|
@ -21,3 +21,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-tip {
|
||||
.tooltip-inner {
|
||||
max-width: fit-content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ const Index = ({
|
|||
cancelBtnVariant={cancelBtnVariant}
|
||||
confirmBtnVariant={confirmBtnVariant}
|
||||
{...props}>
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<p dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</Modal>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ const Index: FC<IProps> = ({
|
|||
if (type === 'question') {
|
||||
Modal.confirm({
|
||||
title: t('title'),
|
||||
content: hasAnswer ? `<p>${t('question')}</p>` : `<p>${t('other')}</p>`,
|
||||
content: hasAnswer ? t('question') : t('other'),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
@ -90,7 +90,7 @@ const Index: FC<IProps> = ({
|
|||
if (type === 'answer' && aid) {
|
||||
Modal.confirm({
|
||||
title: t('title'),
|
||||
content: isAccepted ? t('answer_accepted') : `<p>${t('other')}</p>`,
|
||||
content: isAccepted ? t('answer_accepted') : t('other'),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
@ -128,6 +128,7 @@ const Index: FC<IProps> = ({
|
|||
title: t('title', { keyPrefix: 'question_detail.reopen' }),
|
||||
content: t('content', { keyPrefix: 'question_detail.reopen' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.reopen' }),
|
||||
onConfirm: () => {
|
||||
reopenQuestion({
|
||||
question_id: qid,
|
||||
|
|
|
@ -58,7 +58,7 @@ const Answers: FC = () => {
|
|||
content:
|
||||
item.accepted === 2
|
||||
? t('answer_accepted', { keyPrefix: 'delete' })
|
||||
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
|
||||
: t('other', { keyPrefix: 'delete' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
|
|
@ -67,8 +67,8 @@ const Questions: FC = () => {
|
|||
title: t('title', { keyPrefix: 'delete' }),
|
||||
content:
|
||||
item.answer_count > 0
|
||||
? `<p>${t('question', { keyPrefix: 'delete' })}</p>`
|
||||
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
|
||||
? t('question', { keyPrefix: 'delete' })
|
||||
: t('other', { keyPrefix: 'delete' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
|
|
@ -19,14 +19,14 @@ const Index: FC = () => {
|
|||
type: 'number',
|
||||
title: t('permalink.label'),
|
||||
description: t('permalink.text'),
|
||||
enum: [1, 2, 3, 4],
|
||||
enum: [4, 3, 2, 1],
|
||||
enumNames: [
|
||||
'/questions/10010000000000001/post-title',
|
||||
'/questions/10010000000000001',
|
||||
'/questions/D1D1/post-title',
|
||||
'/questions/D1D1',
|
||||
'/questions/D1D1/post-title',
|
||||
'/questions/10010000000000001',
|
||||
'/questions/10010000000000001/post-title',
|
||||
],
|
||||
default: 1,
|
||||
default: 4,
|
||||
},
|
||||
robots: {
|
||||
type: 'string',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, FC, useEffect, useRef } from 'react';
|
||||
import { Button, Alert } from 'react-bootstrap';
|
||||
import { Button, Alert, Badge } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
@ -20,8 +20,7 @@ interface Props {
|
|||
data: AnswerItem;
|
||||
/** router answer id */
|
||||
aid?: string;
|
||||
/** is author */
|
||||
isAuthor: boolean;
|
||||
canAccept: boolean;
|
||||
questionTitle: string;
|
||||
slugTitle: string;
|
||||
isLogged: boolean;
|
||||
|
@ -30,11 +29,11 @@ interface Props {
|
|||
const Index: FC<Props> = ({
|
||||
aid,
|
||||
data,
|
||||
isAuthor,
|
||||
isLogged,
|
||||
questionTitle = '',
|
||||
slugTitle,
|
||||
callback,
|
||||
canAccept = false,
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'question_detail',
|
||||
|
@ -77,12 +76,21 @@ const Index: FC<Props> = ({
|
|||
{t('post_deleted', { keyPrefix: 'messages' })}
|
||||
</Alert>
|
||||
)}
|
||||
{data?.accepted === 2 && (
|
||||
<div style={{ lineHeight: '20px' }} className="mb-3">
|
||||
<Badge bg="success" pill>
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
Best answer
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<article
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt text-break text-wrap"
|
||||
/>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<Actions
|
||||
source="answer"
|
||||
data={{
|
||||
id: data?.id,
|
||||
isHate: data?.vote_status === 'vote_down',
|
||||
|
@ -95,24 +103,17 @@ const Index: FC<Props> = ({
|
|||
}}
|
||||
/>
|
||||
|
||||
{data?.accepted === 2 && (
|
||||
{canAccept && (
|
||||
<Button
|
||||
disabled={!isAuthor}
|
||||
variant="outline-success"
|
||||
className="ms-3 active opacity-100 bg-success text-white"
|
||||
onClick={acceptAnswer}>
|
||||
<Icon name="check-circle-fill" className="me-2" />
|
||||
<span>{t('answers.btn_accepted')}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isAuthor && data.accepted === 1 && (
|
||||
<Button
|
||||
variant="outline-success"
|
||||
variant={data.accepted === 2 ? 'success' : 'outline-success'}
|
||||
className="ms-3"
|
||||
onClick={acceptAnswer}>
|
||||
<Icon name="check-circle-fill" className="me-2" />
|
||||
<span>{t('answers.btn_accept')}</span>
|
||||
<span>
|
||||
{data.accepted === 2
|
||||
? t('answers.btn_accepted')
|
||||
: t('answers.btn_accept')}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -114,6 +114,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
|
||||
<Actions
|
||||
className="mt-4"
|
||||
source="question"
|
||||
data={{
|
||||
id: data?.id,
|
||||
isHate: data?.vote_status === 'vote_down',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { memo, useState, FC, useEffect } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Alert } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import classNames from 'classnames';
|
||||
|
@ -18,6 +18,7 @@ interface Props {
|
|||
/** question id */
|
||||
qid: string;
|
||||
answered?: boolean;
|
||||
loggedUserRank: number;
|
||||
};
|
||||
callback?: (obj) => void;
|
||||
}
|
||||
|
@ -39,6 +40,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
const [showTips, setShowTips] = useState(data.loggedUserRank < 100);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: Boolean(formData.content.value),
|
||||
|
@ -212,29 +214,58 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
</div>
|
||||
)}
|
||||
{showEditor && (
|
||||
<Editor
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
value: val,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Editor
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
value: val,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
variant="warning"
|
||||
show={data.loggedUserRank < 100 && showTips}
|
||||
onClose={() => setShowTips(false)}
|
||||
dismissible
|
||||
className="mt-3">
|
||||
<p>{t('tips.header_1')}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="question_detail.write_answer.tips.li1_1"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>{t('tips.li1_2')}</li>
|
||||
</ul>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="question_detail.write_answer.tips.header_2"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
<ul>
|
||||
<li>{t('tips.li2_1')}</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
|
|
|
@ -56,7 +56,9 @@ const Index = () => {
|
|||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const isAdmin = userInfo?.role_id === 2;
|
||||
const isModerator = userInfo?.role_id === 3;
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const loggedUserRank = userInfo?.rank;
|
||||
const { state: locationState } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -221,7 +223,7 @@ const Index = () => {
|
|||
data={item}
|
||||
questionTitle={question?.title || ''}
|
||||
slugTitle={question?.url_title}
|
||||
isAuthor={isAuthor}
|
||||
canAccept={isAuthor || isAdmin || isModerator}
|
||||
callback={initPage}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
|
@ -247,6 +249,7 @@ const Index = () => {
|
|||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
loggedUserRank,
|
||||
}}
|
||||
callback={writeAnswerCallback}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -23,18 +23,7 @@ interface FormDataItem {
|
|||
content: Type.FormValue<string>;
|
||||
description: Type.FormValue<string>;
|
||||
}
|
||||
const initFormData = {
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const { aid = '', qid = '' } = useParams();
|
||||
const [focusType, setForceType] = useState('');
|
||||
|
@ -42,12 +31,36 @@ const Index = () => {
|
|||
const { t } = useTranslation('translation', { keyPrefix: 'edit_answer' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const initFormData = {
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { data } = useQueryAnswerInfo(aid);
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData, setImmData] = useState(initFormData);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
|
||||
initFormData.content.value = data?.info.content || '';
|
||||
useLayoutEffect(() => {
|
||||
if (data?.info?.content) {
|
||||
setFormData({
|
||||
...formData,
|
||||
content: {
|
||||
value: data.info.content,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [data?.info?.content]);
|
||||
|
||||
const { data: revisions = [] } = useQueryRevisions(aid);
|
||||
|
||||
|
@ -147,9 +160,11 @@ const Index = () => {
|
|||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
if (revision?.content) {
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
};
|
||||
|
||||
const backPage = () => {
|
||||
|
@ -192,7 +207,7 @@ const Index = () => {
|
|||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="revision" className="mb-3">
|
||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||
<Form.Select onChange={handleSelectedRevision}>
|
||||
<Form.Select onChange={handleSelectedRevision} defaultValue={0}>
|
||||
{revisions.map(({ create_at, reason, user_info }, index) => {
|
||||
const date = dayjs(create_at * 1000)
|
||||
.tz()
|
||||
|
|
Loading…
Reference in New Issue