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

This commit is contained in:
aichy126
2023-01-30 15:50:19 +08:00
20 changed files with 1508 additions and 195 deletions
+1 -1
View File
@@ -39,6 +39,6 @@ install-ui-packages:
@corepack prepare pnpm@v7.12.2 --activate
ui:
@cd ui && pnpm install && pnpm build && cd -
@cd ui && pnpm install && pnpm build && sed -i 's/%AnswerVersion%/'$(VERSION)'/g' ./build/index.html && cd -
all: clean build
+7 -3
View File
@@ -640,7 +640,7 @@ ui:
msg:
empty: Email cannot be empty.
change_email:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
btn_cancel: Cancel
btn_update: Update email address
send_success: >-
@@ -840,7 +840,7 @@ ui:
modal_confirm:
title: Error...
account_result:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
success: Your new account is confirmed; you will be redirected to the home page.
link: Continue to homepage
invalid: >-
@@ -996,7 +996,11 @@ ui:
db_failed: Database connection failed
db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down.
counts:
views: views
votes: votes
answers: answers
accepted: Accepted
page_404:
desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage
+1 -1
View File
@@ -33,7 +33,7 @@ var (
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
}
+3
View File
@@ -22,7 +22,9 @@
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"diff": "^5.1.0",
"dompurify": "^2.4.3",
"emoji-regex": "^10.2.1",
"html-react-parser": "^3.0.8",
"i18next": "^21.9.0",
"katex": "^0.16.2",
"lodash": "^4.17.21",
@@ -51,6 +53,7 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/color": "^3.0.3",
"@types/dompurify": "^2.4.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.184",
"@types/marked": "^4.0.6",
+1319 -93
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="generator" content="Answer %AnswerVersion% - https://github.com/answerdev/answer">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
</head>
<body>
+77
View File
@@ -0,0 +1,77 @@
import { FC, memo } from 'react';
import { useTranslation } from 'react-i18next';
import classname from 'classnames';
import { Icon } from '@/components';
interface Props {
data: {
votes: number;
answers: number;
views: number;
};
showVotes?: boolean;
showAnswers?: boolean;
showViews?: boolean;
showAccepted?: boolean;
isAccepted?: boolean;
className?: string;
}
const Index: FC<Props> = ({
data,
showVotes = true,
showAnswers = true,
showViews = true,
isAccepted = false,
showAccepted = false,
className = '',
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'counts' });
return (
<div className={classname('d-flex align-items-center', className)}>
{showVotes && (
<div className="d-flex align-items-center">
<Icon name="hand-thumbs-up-fill me-1" />
<span>
{data.votes} {t('votes')}
</span>
</div>
)}
{showAccepted && (
<div className="d-flex align-items-center ms-3 text-success">
<Icon name="check-circle-fill me-1" />
<span>{t('accepted')}</span>
</div>
)}
{showAnswers && (
<div
className={`d-flex align-items-center ms-3 ${
isAccepted ? 'text-success' : ''
}`}>
{isAccepted ? (
<Icon name="check-circle-fill me-1" />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>
{data.answers} {t('answers')}
</span>
</div>
)}
{showViews && (
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">
{data.views} {t('views')}
</em>
</span>
)}
</div>
);
};
export default memo(Index);
+5 -3
View File
@@ -8,6 +8,7 @@ import {
} from 'react';
import { markdownToHtml } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from './utils';
@@ -38,6 +39,7 @@ const Index = ({ value }, ref) => {
}
previewRef.current?.scrollTo(0, scrollTop);
htmlRender(previewRef.current);
}, [html]);
useImperativeHandle(ref, () => {
@@ -49,9 +51,9 @@ const Index = ({ value }, ref) => {
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
{htmlToReact(html)}
</div>
);
};
+10 -24
View File
@@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
import { pathFactory } from '@/router/pathFactory';
import type * as Type from '@/common/interface';
import {
Icon,
Tag,
Pagination,
FormatTime,
@@ -14,6 +13,7 @@ import {
BaseUserCard,
QueryGroup,
QuestionListLoader,
Counts,
} from '@/components';
import { useQuestionList } from '@/services';
@@ -95,29 +95,15 @@ const QuestionList: FC<Props> = ({ source }) => {
preFix={t(li.operation_type)}
/>
</div>
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
<span>
<Icon name="hand-thumbs-up-fill" />
<em className="fst-normal ms-1">{li.vote_count}</em>
</span>
<span
className={`ms-3 ${
li.accepted_answer_id >= 1 ? 'text-success' : ''
}`}>
<Icon
name={
li.accepted_answer_id >= 1
? 'check-circle-fill'
: 'chat-square-text-fill'
}
/>
<em className="fst-normal ms-1">{li.answer_count}</em>
</span>
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">{li.view_count}</em>
</span>
</div>
<Counts
data={{
votes: li.vote_count,
answers: li.answer_count,
views: li.view_count,
}}
isAccepted={li.accepted_answer_id >= 1}
className="ms-0 ms-md-3 mt-2 mt-md-0"
/>
</div>
<div className="question-tags m-n1">
{Array.isArray(li.tags)
+14 -6
View File
@@ -155,6 +155,7 @@ const TagSelector: FC<IProps> = ({
};
const handleKeyDown = (e) => {
e.stopPropagation();
if (!tags) {
return;
}
@@ -166,13 +167,20 @@ const TagSelector: FC<IProps> = ({
if (keyCode === 40 && currentIndex < tags.length - 1) {
setCurrentIndex(currentIndex + 1);
}
if (
keyCode === 13 &&
currentIndex > -1 &&
currentIndex <= tags.length - 1
) {
if (keyCode === 13 && currentIndex > -1) {
e.preventDefault();
handleClick(tags[currentIndex]);
if (tags.length === 0) {
tagModal.onShow(tag);
return;
}
if (currentIndex <= tags.length - 1) {
handleClick(tags[currentIndex]);
if (currentIndex === tags.length - 1 && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
}
}
};
return (
+2
View File
@@ -32,6 +32,7 @@ import CustomizeTheme from './CustomizeTheme';
import PageTags from './PageTags';
import QuestionListLoader from './QuestionListLoader';
import TagsLoader from './TagsLoader';
import Counts from './Counts';
export {
Avatar,
@@ -70,5 +71,6 @@ export {
PageTags,
QuestionListLoader,
TagsLoader,
Counts,
};
export type { EditorRef, JSONSchema, UISchema };
@@ -3,7 +3,7 @@ import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { pathFactory } from '@/router/pathFactory';
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
import { Tag, FormatTime, BaseUserCard, Counts } from '@/components';
import type { SearchResItem } from '@/common/interface';
import { escapeRemove } from '@/utils';
@@ -51,23 +51,17 @@ const Index: FC<Props> = ({ data }) => {
className="me-3"
preFix={data.object_type === 'question' ? 'asked' : 'answered'}
/>
<div className="d-flex align-items-center my-2 my-sm-0">
<div className="d-flex align-items-center me-3">
<Icon name="hand-thumbs-up-fill me-1" />
<span> {data.object?.vote_count}</span>
</div>
<div
className={`d-flex align-items-center ${
data.object?.accepted ? 'text-success' : ''
}`}>
{data.object?.accepted ? (
<Icon name="check-circle-fill me-1" />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>{data.object?.answer_count}</span>
</div>
</div>
<Counts
className="my-2 my-sm-0"
showViews={false}
isAccepted={data.object?.accepted}
data={{
votes: data.object?.vote_count,
answers: data.object?.answer_count,
views: 0,
}}
/>
</div>
{data.object?.excerpt && (
@@ -3,10 +3,12 @@ import { Container, Row, Col } from 'react-bootstrap';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { siteInfoStore } from '@/stores';
import { usePageTags } from '@/hooks';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
const siteName = siteInfoStore((state) => state.siteInfo.name);
const location = useLocation();
usePageTags({
title: t('account_activation', { keyPrefix: 'page_title' }),
@@ -15,7 +17,9 @@ const Index: FC = () => {
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col lg={6}>
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
<h3 className="text-center mt-3 mb-5">
{t('page_title', { site_name: siteName })}
</h3>
{location.pathname?.includes('success') && (
<>
<p className="text-center">{t('success')}</p>
+5 -1
View File
@@ -2,18 +2,22 @@ import { FC, memo } from 'react';
import { Container, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { siteInfoStore } from '@/stores';
import { usePageTags } from '@/hooks';
import SendEmail from './components/sendEmail';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
const siteName = siteInfoStore((state) => state.siteInfo.name);
usePageTags({
title: t('change_email', { keyPrefix: 'page_title' }),
});
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<h3 className="text-center mb-5">
{t('page_title', { site_name: siteName })}
</h3>
<Col className="mx-auto" md={3}>
<SendEmail />
</Col>
+5 -2
View File
@@ -4,7 +4,7 @@ import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores';
import { loggedUserInfoStore, siteInfoStore } from '@/stores';
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
const Index: FC = () => {
@@ -13,6 +13,7 @@ const Index: FC = () => {
const [step, setStep] = useState('loading');
const updateUser = loggedUserInfoStore((state) => state.update);
const siteName = siteInfoStore((state) => state.siteInfo.name);
useEffect(() => {
const code = searchParams.get('code');
@@ -38,7 +39,9 @@ const Index: FC = () => {
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col lg={6}>
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
<h3 className="text-center mt-3 mb-5">
{t('page_title', { site_name: siteName })}
</h3>
{step === 'success' && (
<>
<p className="text-center">{t('confirm_new_email')}</p>
@@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag } from '@/components';
import { FormatTime, Tag, Counts } from '@/components';
import { pathFactory } from '@/router/pathFactory';
interface Props {
@@ -35,21 +35,16 @@ const Index: FC<Props> = ({ visible, data }) => {
<div className="d-flex align-items-center fs-14 text-secondary mb-2">
<FormatTime
time={item.create_time}
className="me-4"
className="me-3"
preFix={t('answered')}
/>
<div className="d-flex align-items-center me-3">
<Icon name="hand-thumbs-up-fill me-1" />
<span>{item?.vote_count}</span>
</div>
{item.accepted === 2 && (
<div className="d-flex align-items-center me-3 text-success">
<Icon name="check-circle-fill me-1" />
<span>{t('accepted')}</span>
</div>
)}
<Counts
data={{ votes: item?.vote_count, views: 0, answers: 0 }}
showAnswers={false}
showViews={false}
showAccepted={item.accepted === 2}
/>
</div>
<div>
{item.question_info?.tags?.map((tag) => {
@@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag, BaseUserCard } from '@/components';
import { FormatTime, Tag, BaseUserCard, Counts } from '@/components';
import { pathFactory } from '@/router/pathFactory';
interface Props {
@@ -44,35 +44,23 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
<span className="split-dot" />
</>
)}
<FormatTime
time={item.create_time}
time={
tabName === 'bookmarks' ? item.create_time : item.created_at
}
className="me-3"
preFix={t('asked')}
/>
<div className="d-flex align-items-center me-3">
<Icon name="hand-thumbs-up-fill me-1" />
<span>{item.vote_count}</span>
</div>
{tabName !== 'answers' && (
<div
className={`d-flex align-items-center me-3 ${
Number(item.accepted_answer_id) > 0 ? 'text-success' : ''
}`}>
{Number(item.accepted_answer_id) > 0 ? (
<Icon name="check-circle-fill me-1" />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>{item.answer_count}</span>
</div>
)}
<div className="d-flex align-items-center me-3">
<Icon name="eye-fill me-1" />
<span>{item.view_count}</span>
</div>
<Counts
isAccepted={Number(item.accepted_answer_id) > 0}
data={{
votes: item.vote_count,
answers: item.answer_count,
views: item.view_count,
}}
/>
</div>
<div>
{item.tags?.map((tag) => {
@@ -32,9 +32,12 @@ const Index: FC<Props> = ({ data, type }) => {
}>
{type === 'answer' ? item.question_info.title : item.title}
</a>
<div className="d-inline-block text-secondary ms-3 fs-14">
<Icon name="hand-thumbs-up-fill" />
<span> {item.vote_count}</span>
<Icon name="hand-thumbs-up-fill me-1" />
<span>
{item.vote_count} {t('votes', { keyPrefix: 'counts' })}
</span>
</div>
{type === 'question' && (
<div
@@ -47,7 +50,10 @@ const Index: FC<Props> = ({ data, type }) => {
<Icon name="chat-square-text-fill" />
)}
<span> {item.answer_count}</span>
<span>
{' '}
{item.answer_count} {t('answers', { keyPrefix: 'counts' })}
</span>
</div>
)}
+10
View File
@@ -1,4 +1,6 @@
import i18next from 'i18next';
import parse from 'html-react-parser';
import * as DOMPurify from 'dompurify';
const Diff = require('diff');
@@ -214,6 +216,13 @@ function diffText(newText: string, oldText: string): string {
?.replace(/<input/gi, '&lt;input');
}
function htmlToReact(html: string) {
const cleanedHtml = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
return parse(cleanedHtml);
}
export {
thousandthDivision,
formatCount,
@@ -228,4 +237,5 @@ export {
labelStyle,
handleFormError,
diffText,
htmlToReact,
};
+1 -1
View File
@@ -267,7 +267,7 @@ export const initAppSettingsStore = async () => {
};
export const shouldInitAppFetchData = () => {
if (isIgnoredPath('/install')) {
if (isIgnoredPath('/install') && window.location.pathname === '/install') {
return false;
}