Merge branch 'feat/ui-0.7.0' into 'test'

Feat/ui 0.7.0

See merge request opensource/answer!353
This commit is contained in:
贾海涛(龙笛)
2022-12-16 09:48:16 +00:00
26 changed files with 140 additions and 57 deletions
+65 -9
View File
@@ -4,7 +4,7 @@
To learn more about the philosophy and goals of the project, visit [Answer](https://answer.dev).
### 📦 Prerequisites
## ⚙️ Prerequisites
- [Node.js](https://nodejs.org/) `>=16.17`
- [pnpm](https://pnpm.io/) `>=7`
@@ -40,22 +40,78 @@ you can also manually visit it.
when cloning repo, and run `pnpm install` to init dependencies. you can use project commands below:
- `pnpm run start` run Answer web locally.
- `pnpm run build` build Answer for production
- `pnpm run lint` lint and fix the code style
- `pnpm start` run Answer web locally.
- `pnpm build` build Answer for production
- `pnpm lint` lint and fix the code style
## 🌍 I18n(Multi-language)
If you need to add or edit a language entry, just go to the `/i18n/en_US.yaml` file,
all front-end language entries are placed under the `ui` field.
## 🖥 Environment Support
If you would like to help us with the i18n translation, please visit [Answer@Crowdin](https://crowdin.com/translate/answer)
## 💡 Project instructions
```
.
├── cmd
├── configs
├── docs
├── i18n
├── en_US.yaml (basic language file)
├── i18n.yaml (language list)
├── internal
├── ...
└── ui (front-end project starts here)
├── build (built results directory, usually without concern)
├── public (html template for public)
├── scripts (some scripting tools on front-end project)
├── src (almost all front-end resources are here)
├── assets (static resources)
├── common (project information/data defined here)
├── components (all components of the project)
├── hooks (all hooks of the project)
├── i18n (Initialize the front-end i18n)
├── pages (all pages of the project)
├── router (Project routing definition)
├── services (all data api of the project)
├── stores (all data stores of the project)
├── utils (all utils of the project)
```
## 🤝 Contributing
#### Fix Bug
If you find a bug, please don't hesitate to [submit an issue](https://github.com/answerdev/answer/issues) to us.
If you can fix it, please include a note with your issue submission.
If it is a bug definitely, you can submit your PR after we confirm it, which will ensure you don't do anything useless.
#### Code Review & Comment
In our development, some codes are not logical we know. If you find it, please don't hesitate to submit PR to us.
In the same way, some function has no comment. We would appreciate it if you could help us supplement it.
#### Translation
All our translations are placed in the i18n directory.
1. If you find that the corresponding key in the language you are using does not have a translation, you can submit your translation.
2. If you want to submit a new language translation, please add your language to the `i18n.yaml` file.
#### Features or Plugin
1. We developed the features for the plan based on the [roadmap](https://github.com/orgs/answerdev/projects/1). If you are suggestions for new functions, please confirm whether they have been planned.
2. Plugins will be available in the future, so stay tuned.
## 📱Environment Support
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 🧱 Build with
- [React.js](https://reactjs.org/) - Our front end is a React.js app.
## Built with
- [TypeScript](https://www.typescriptlang.org/) - strongly typed JavaScript
- [React.js](https://reactjs.org/) - Our front end is a React.js app
- [React Router](https://reactrouter.com/en/main) - Router library
- [Bootstrap](https://getbootstrap.com/) - UI library.
- [React Bootstrap](https://react-bootstrap.github.io/) - UI Library(rebuilt for React.)
- [React Bootstrap](https://react-bootstrap.github.io/) - UI Library(rebuilt for React)
- [axios](https://github.com/axios/axios) - Request library
- [SWR](https://swr.bootcss.com/) - Request library
- [react-i18next](https://react.i18next.com/) - International library
+4
View File
@@ -54,6 +54,7 @@ export interface TagInfo extends TagBase {
}
export interface QuestionParams {
title: string;
url_title?: string;
content: string;
html: string;
tags: Tag[];
@@ -396,6 +397,7 @@ export interface SearchParams {
export interface SearchResItem {
object_type: string;
object: {
url_title?: string;
id: string;
question_id?: string;
title: string;
@@ -455,6 +457,7 @@ export interface TimelineItem {
export interface TimelineObject {
title: string;
url_title?: string;
object_type: string;
question_id: string;
answer_id: string;
@@ -470,6 +473,7 @@ export interface TimelineRes {
export interface ReviewItem {
type: 'question' | 'answer' | 'tag';
info: {
url_title?: string;
object_id: string;
title: string;
content: string;
+1 -1
View File
@@ -30,7 +30,7 @@ const HotQuestions: FC = () => {
<ListGroupItem
key={li.id}
as={Link}
to={pathFactory.questionLanding(li.id, li.title)}
to={pathFactory.questionLanding(li.id, li.url_title)}
action>
<div className="link-dark">{li.title}</div>
{li.answer_count > 0 ? (
+9 -1
View File
@@ -19,6 +19,7 @@ interface IProps {
qid: string;
aid?: string;
title: string;
slugTitle: string;
hasAnswer?: boolean;
isAccepted: boolean;
callback: (type: string) => void;
@@ -29,6 +30,7 @@ const Index: FC<IProps> = ({
qid,
aid = '',
title,
slugTitle,
isAccepted = false,
hasAnswer = false,
memberActions = [],
@@ -159,7 +161,13 @@ const Index: FC<IProps> = ({
return (
<div className="d-flex align-items-center">
<Share type={type} qid={qid} aid={aid} title={title} />
<Share
type={type}
qid={qid}
aid={aid}
title={title}
slugTitle={slugTitle}
/>
{memberActions?.map((item) => {
if (item.action === 'edit') {
return (
+1 -1
View File
@@ -123,7 +123,7 @@ const QuestionList: FC<Props> = ({ source }) => {
<ListGroup.Item key={li.id} className="border-bottom py-3 px-0">
<h5 className="text-wrap text-break">
<NavLink
to={pathFactory.questionLanding(li.id, li.title)}
to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark">
{li.title}
{li.status === 2 ? ` [${t('closed')}]` : ''}
+8 -4
View File
@@ -13,21 +13,25 @@ interface IProps {
qid: any;
aid?: any;
title: string;
slugTitle: string;
}
const Index: FC<IProps> = ({ type, qid, aid, title }) => {
const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
const user = loggedUserInfoStore((state) => state.user);
const [show, setShow] = useState(false);
const [showTip, setShowTip] = useState(false);
const [canSystemShare, setSystemShareState] = useState(false);
const { t } = useTranslation();
// FIXME: pathFactory
let baseUrl =
type === 'question'
? `${window.location.origin}${pathFactory.questionLanding(qid, title)}`
? `${window.location.origin}${pathFactory.questionLanding(
qid,
slugTitle,
)}`
: `${window.location.origin}${pathFactory.answerLanding({
questionId: qid,
questionTitle: title,
slugTitle,
answerId: aid,
})}`;
if (user.id) {
+1 -1
View File
@@ -130,7 +130,7 @@ const Answers: FC = () => {
<a
href={pathFactory.answerLanding({
questionId: li.question_id,
questionTitle: li.question_info.title,
slugTitle: li.question_info.url_title,
answerId: li.id,
})}
target="_blank"
+1 -1
View File
@@ -102,7 +102,7 @@ const Flags: FC = () => {
<a
href={pathFactory.questionLanding(
li.question_id,
li.title,
li.url_title,
)}
target="_blank"
className="text-wrap text-break mt-2"
+1 -1
View File
@@ -137,7 +137,7 @@ const Questions: FC = () => {
<tr key={li.id}>
<td>
<a
href={pathFactory.questionLanding(li.id, li.title)}
href={pathFactory.questionLanding(li.id, li.url_title)}
target="_blank"
className="text-break text-wrap"
rel="noreferrer">
@@ -29,7 +29,7 @@ const SearchQuestion = ({ similarQuestions }) => {
as="a"
className="link-dark"
key={item.id}
href={pathFactory.questionLanding(item.id, item.title)}
href={pathFactory.questionLanding(item.id, item.url_title)}
target="_blank">
<span className="text-wrap text-break">
{item.title}
+3 -3
View File
@@ -237,7 +237,7 @@ const Ask = () => {
edit_summary: formData.edit_summary.value,
})
.then((res) => {
navigate(pathFactory.questionLanding(qid, params.title), {
navigate(pathFactory.questionLanding(qid, params.url_title), {
state: { isReview: res?.wait_for_review },
});
})
@@ -264,7 +264,7 @@ const Ask = () => {
html: editorRef2.current.getHtml(),
})
.then(() => {
navigate(pathFactory.questionLanding(id, params.title));
navigate(pathFactory.questionLanding(id, params.url_title));
})
.catch((err) => {
if (err.isError) {
@@ -273,7 +273,7 @@ const Ask = () => {
}
});
} else {
navigate(pathFactory.questionLanding(id, params.title));
navigate(pathFactory.questionLanding(id));
}
}
}
@@ -23,6 +23,7 @@ interface Props {
/** is author */
isAuthor: boolean;
questionTitle: string;
slugTitle: string;
isLogged: boolean;
callback: (type: string) => void;
}
@@ -32,6 +33,7 @@ const Index: FC<Props> = ({
isAuthor,
isLogged,
questionTitle = '',
slugTitle,
callback,
}) => {
const { t } = useTranslation('translation', {
@@ -114,6 +116,7 @@ const Index: FC<Props> = ({
type="answer"
isAccepted={data.adopted === 2}
title={questionTitle}
slugTitle={slugTitle}
callback={callback}
/>
</Col>
@@ -65,7 +65,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
<Link
className="link-dark"
reloadDocument
to={pathFactory.questionLanding(data.id, data.title)}>
to={pathFactory.questionLanding(data.id, data.url_title)}>
{data.title}
{data.status === 2
? ` [${t('closed', { keyPrefix: 'question' })}]`
@@ -129,6 +129,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
type="question"
memberActions={data?.member_actions}
title={data.title}
slugTitle={data.url_title}
hasAnswer={hasAnswer}
isAccepted={Boolean(data?.accepted_answer_id)}
callback={initPage}
@@ -32,7 +32,7 @@ const Index: FC<Props> = ({ id }) => {
action
key={item.id}
as={Link}
to={pathFactory.questionLanding(item.id, item.title)}>
to={pathFactory.questionLanding(item.id, item.url_title)}>
<div className="link-dark">{item.title}</div>
{item.answer_count > 0 && (
<div
+1
View File
@@ -178,6 +178,7 @@ const Index = () => {
key={item?.id}
data={item}
questionTitle={question?.title || ''}
slugTitle={question?.url_title}
isAuthor={isAuthor}
callback={initPage}
isLogged={isLogged}
+2 -2
View File
@@ -115,7 +115,7 @@ const Index = () => {
navigate(
pathFactory.answerLanding({
questionId: qid,
questionTitle: data?.question?.title,
slugTitle: data?.question?.url_title,
answerId: aid,
}),
{
@@ -147,7 +147,7 @@ const Index = () => {
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<a
href={pathFactory.questionLanding(qid, data?.question.title)}
href={pathFactory.questionLanding(qid, data?.question.url_title)}
target="_blank"
rel="noreferrer">
<h5 className="mb-3">{data?.question.title}</h5>
+2 -2
View File
@@ -95,7 +95,7 @@ const Index: FC = () => {
const editor = unreviewed_info?.user_info;
const editTime = unreviewed_info?.create_at;
if (type === 'question') {
itemLink = pathFactory.questionLanding(info?.object_id, info?.title);
itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);
itemTitle = info?.title;
editBadge = t('question_edit');
editSummary ||= t('edit_question');
@@ -103,7 +103,7 @@ const Index: FC = () => {
itemLink = pathFactory.answerLanding({
// @ts-ignore
questionId: unreviewed_info.content.question_id,
questionTitle: info?.title,
slugTitle: info?.url_title,
answerId: unreviewed_info.object_id,
});
itemTitle = info?.title;
@@ -15,11 +15,14 @@ const Index: FC<Props> = ({ data }) => {
if (!data?.object_type) {
return null;
}
let itemUrl = pathFactory.questionLanding(data.object.id, data.object.title);
let itemUrl = pathFactory.questionLanding(
data.object.id,
data.object.url_title,
);
if (data.object_type === 'answer' && data.object.question_id) {
itemUrl = pathFactory.answerLanding({
questionId: data.object.question_id,
questionTitle: data.object.title,
slugTitle: data.object.url_title,
answerId: data.object.id,
});
}
+2 -2
View File
@@ -48,7 +48,7 @@ const Index: FC = () => {
if (timelineData?.object_info.object_type === 'question') {
linkUrl = pathFactory.questionLanding(
timelineData?.object_info.question_id,
timelineData?.object_info.title,
timelineData?.object_info.url_title,
);
pageTitle = `${t('title_for_question')} ${timelineData?.object_info.title}`;
}
@@ -56,7 +56,7 @@ const Index: FC = () => {
if (timelineData?.object_info.object_type === 'answer') {
linkUrl = pathFactory.answerLanding({
questionId: timelineData?.object_info.question_id,
questionTitle: timelineData?.object_info.title,
slugTitle: timelineData?.object_info.url_title,
answerId: timelineData?.object_info.answer_id,
});
pageTitle = `${t('title_for_answer', {
@@ -23,7 +23,7 @@ const Index: FC<Props> = ({ visible, data }) => {
<a
href={pathFactory.answerLanding({
questionId: item.question_id,
questionTitle: item.question_info?.title,
slugTitle: item.question_info?.url_title,
answerId: item.answer_id,
})}
className="text-break">
@@ -22,10 +22,10 @@ const Index: FC<Props> = ({ visible, data }) => {
className="text-break"
href={
item.object_type === 'question'
? pathFactory.questionLanding(item.object_id, item.title)
? pathFactory.questionLanding(item.object_id, item.url_title)
: pathFactory.answerLanding({
questionId: item.question_id,
questionTitle: item.title,
slugTitle: item.url_title,
answerId: item.object_id,
})
}>
@@ -29,7 +29,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
className="text-break"
href={pathFactory.questionLanding(
tabName === 'questions' ? item.question_id : item.id,
item.title,
item.url_title,
)}>
{item.title}
{tabName === 'questions' && item.status === 'closed'
@@ -33,10 +33,13 @@ const Index: FC<Props> = ({ visible, data }) => {
className="text-break"
href={
item.object_type === 'question'
? pathFactory.questionLanding(item.object_id, item.title)
? pathFactory.questionLanding(
item.object_id,
item.url_title,
)
: pathFactory.answerLanding({
questionId: item.question_id,
questionTitle: item.title,
slugTitle: item.url_title,
answerId: item.object_id,
})
}>
@@ -23,10 +23,13 @@ const Index: FC<Props> = ({ data, type }) => {
type === 'answer'
? pathFactory.answerLanding({
questionId: item.question_id,
questionTitle: item.question_info?.title,
slugTitle: item.question_info?.url_title,
answerId: item.answer_id,
})
: pathFactory.questionLanding(item.question_id, item.title)
: pathFactory.questionLanding(
item.question_id,
item.url_title,
)
}>
{type === 'answer' ? item.question_info.title : item.title}
</a>
@@ -29,10 +29,13 @@ const Index: FC<Props> = ({ visible, data }) => {
className="text-break"
href={
item.object_type === 'question'
? pathFactory.questionLanding(item.question_id, item.title)
? pathFactory.questionLanding(
item.question_id,
item.url_title,
)
: pathFactory.answerLanding({
questionId: item.question_id,
questionTitle: item.title,
slugTitle: item.url_title,
answerId: item.answer_id,
})
}>
+8 -14
View File
@@ -1,6 +1,5 @@
import urlcat from 'urlcat';
import Pattern from '@/common/pattern';
import { seoSettingStore } from '@/stores';
const tagLanding = (slugName: string) => {
@@ -20,30 +19,25 @@ const tagInfo = (slugName: string) => {
const tagEdit = (tagId: string) => {
return urlcat('/tags/:tagId/edit', { tagId });
};
const questionLanding = (questionId: string, title: string = '') => {
const questionLanding = (questionId: string, slugTitle: string = '') => {
const { seo } = seoSettingStore.getState();
if (seo.permalink === 1) {
title = title.toLowerCase();
title = title.trim().replace(/\s+/g, '-');
title = title.replace(Pattern.emoji, '');
if (title) {
return urlcat('/questions/:questionId/:slugPermalink', {
questionId,
slugPermalink: title,
});
}
if (seo.permalink === 1 && slugTitle) {
return urlcat('/questions/:questionId/:slugPermalink', {
questionId,
slugPermalink: slugTitle,
});
}
return urlcat('/questions/:questionId', { questionId });
};
const answerLanding = (params: {
questionId: string;
questionTitle?: string;
slugTitle?: string;
answerId: string;
}) => {
const questionLandingUrl = questionLanding(
params.questionId,
params.questionTitle,
params.slugTitle,
);
return urlcat(`${questionLandingUrl}/:answerId`, {
answerId: params.answerId,