Merge pull request #272 from answerdev/feat/1.0.8/ui

Feat/1.0.8/UI
This commit is contained in:
haitao.jarvis 2023-03-22 18:07:12 +08:00 committed by GitHub
commit 16e0c9d12a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 194 additions and 96 deletions

View File

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

View File

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

View File

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

View File

@ -21,3 +21,9 @@
}
}
}
.comment-tip {
.tooltip-inner {
max-width: fit-content;
}
}

View File

@ -45,7 +45,7 @@ const Index = ({
cancelBtnVariant={cancelBtnVariant}
confirmBtnVariant={confirmBtnVariant}
{...props}>
<div dangerouslySetInnerHTML={{ __html: content }} />
<p dangerouslySetInnerHTML={{ __html: content }} />
</Modal>,
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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