Merge branch 'test' of git.backyard.segmentfault.com:opensource/answer into test
This commit is contained in:
@@ -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
@@ -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 host’s 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
|
||||
|
||||
@@ -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`",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+1319
-93
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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, '<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,
|
||||
};
|
||||
|
||||
@@ -267,7 +267,7 @@ export const initAppSettingsStore = async () => {
|
||||
};
|
||||
|
||||
export const shouldInitAppFetchData = () => {
|
||||
if (isIgnoredPath('/install')) {
|
||||
if (isIgnoredPath('/install') && window.location.pathname === '/install') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user