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

This commit is contained in:
shuai 2022-09-30 17:29:00 +08:00
commit bbc1f88547
12 changed files with 115 additions and 151 deletions

View File

@ -43,7 +43,7 @@ const Index = ({ value }, ref) => {
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2"
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
);

View File

@ -27,11 +27,12 @@ import {
Table,
UL,
} from './ToolBars';
import { createEditorUtils } from './utils';
import { createEditorUtils, htmlRender } from './utils';
import Viewer from './Viewer';
import { CodeMirrorEditor, IEditorContext } from './types';
import { EditorContext } from './EditorContext';
import Editor from './Editor';
import './index.scss';
export interface EditorRef {
@ -157,4 +158,5 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> = (
</>
);
};
export { htmlRender };
export default forwardRef(MDEditor);

View File

@ -1,6 +1,6 @@
import type { Editor, Position } from 'codemirror';
import type CodeMirror from 'codemirror';
import 'highlight.js/styles/github.css';
// import 'highlight.js/styles/github.css';
import 'katex/dist/katex.min.css';
export function createEditorUtils(
@ -82,8 +82,11 @@ export function createEditorUtils(
export function htmlRender(el: HTMLElement | null) {
if (!el) return;
import('mermaid').then(({ default: mermaid }) => {
el.querySelectorAll('.language-mermaid').forEach((pre, index) => {
mermaid.render(`theGraph${index}`, pre.textContent, function (svgCode) {
mermaid.initialize({ startOnLoad: false });
el.querySelectorAll('.language-mermaid').forEach((pre) => {
const flag = Date.now();
mermaid.render(`theGraph${flag}`, pre.textContent, function (svgCode) {
const p = document.createElement('p');
p.className = 'text-center';
p.innerHTML = svgCode;
@ -111,9 +114,9 @@ export function htmlRender(el: HTMLElement | null) {
});
},
);
import('highlight.js').then(({ default: highlight }) => {
el.querySelectorAll('pre code').forEach((code) => {
highlight.highlightElement(code as HTMLElement);
});
});
// import('highlight.js').then(({ default: highlight }) => {
// el.querySelectorAll('pre code').forEach((code) => {
// highlight.highlightElement(code as HTMLElement);
// });
// });
}

View File

@ -1,5 +1,5 @@
import Avatar from './Avatar';
import Editor, { EditorRef } from './Editor';
import Editor, { EditorRef, htmlRender } from './Editor';
import Header from './Header';
import Footer from './Footer';
import Icon from './Icon';
@ -52,5 +52,6 @@ export {
Empty,
BaseUserCard,
FollowingTags,
htmlRender,
};
export type { EditorRef };

View File

@ -1,4 +1,8 @@
{
"how_to_format": {
"title": "How to Format",
"description": "<ul><li><p>to make links</p><pre><code>&lt;https: //url.com&gt;<br/><br/>[Title](https: //url.com)</code></pre></li><li><p>put returns between paragraphs</p></li><li><p><em>italic</em> or **<strong>bold</strong>**</p></li><li><p>indent code by 4 spaces</p></li><li><p>quote by placing &gt; at start of line</p></li><li><p>backtick escapes <code>`like _so_`</code></p></li><li><p>create code fences with backticks `</p><pre><code>``` <br/>code here<br/>```</code></pre></li></ul>"
},
"pagination": {
"prev": "Prev",
"next": "Next"
@ -272,11 +276,7 @@
}
},
"btn_save_edits": "Save edits",
"btn_cancel": "Cancel",
"how_to_format": {
"title": "How to Format",
"description": "This is some text within a card body."
}
"btn_cancel": "Cancel"
},
"dates": {
"long_date": "MMM D",
@ -318,23 +318,7 @@
}
},
"btn_save_edits": "Save edits",
"btn_cancel": "Cancel",
"how_to_ask": {
"title": "How to Ask",
"description": "Not sure what to ask? Recurring questions, onboarding procedures, local setups, and FAQs all make for useful questions."
},
"how_to_format": {
"title": "How to Format",
"description": "This is some text within a card body."
},
"how_to_tag": {
"title": "How to Tag",
"description": "A tag helps ensure that your question will get attention from the right people. Here are tagging tips:",
"tips": [
"If youre not sure what tags to use, check out popular tags.",
"Tag things in more than one way so people can find them more easily. Add tags for product lines, projects, teams, and the specific technologies or languages used."
]
}
"btn_cancel": "Cancel"
},
"tags": {
"title": "Tags",
@ -350,7 +334,7 @@
"more": "More"
},
"ask": {
"title": "Add question",
"title": "Add Question",
"edit_title": "Edit Question",
"default_reason": "Edit question",
"similar_questions": "Similar questions",
@ -390,23 +374,7 @@
"btn_post_question": "Post your question",
"btn_save_edits": "Save edits",
"answer_question": "Answer your own question",
"post_question&answer": "Post your question and answer",
"how_to_ask": {
"title": "How to Ask",
"description": "Not sure what to ask? Recurring questions, onboarding procedures, local setups, and FAQs all make for useful questions."
},
"how_to_format": {
"title": "How to Format",
"description": "This is some text within a card body."
},
"how_to_tag": {
"title": "How to Tag",
"description": "A tag helps ensure that your question will get attention from the right people. Here are tagging tips:",
"tips": [
"If youre not sure what tags to use, check out popular tags.",
"Tag things in more than one way so people can find them more easily. Add tags for product lines, projects, teams, and the specific technologies or languages used."
]
}
"post_question&answer": "Post your question and answer"
},
"tag_selector": {
"add_btn": "Add tag",
@ -874,4 +842,4 @@
}
}
}
}
}

View File

@ -139,3 +139,32 @@ a {
.object-fit-contain {
object-fit: contain;
}
.fmt {
width: 100%;
img {
max-width: 100%;
}
p {
word-break: break-all;
> code {
background-color: #e9ecef;
color: #212529;
padding: 2px 4px;
border-radius: 0.25rem;
}
}
pre {
background-color: #e9ecef;
border-radius: 0.25rem;
padding: 1rem;
}
blockquote {
border-left: 0.25rem solid #ced4da;
padding: 1rem;
color: #6c757d;
background-color: #e9ecef;
> p:last-child {
margin-bottom: 0;
}
}
}

View File

@ -63,7 +63,6 @@ const Ask = () => {
const { qid } = useParams();
const navigate = useNavigate();
const { t } = useTranslation('translation', { keyPrefix: 'ask' });
const { t: t2 } = useTranslation('translation', { keyPrefix: 'dates' });
const isEdit = qid !== undefined;
const { data: similarQuestions = { list: [] } } = useQueryQuestionByTitle(
@ -282,7 +281,7 @@ const Ask = () => {
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ reason, create_at }, index) => {
const date = dayjs(create_at * 1000).format(
t2('long_date_with_time'),
t('long_date_with_time', { keyPrefix: 'dates' }),
);
return (
<option key={`${create_at}`} value={index}>
@ -302,12 +301,6 @@ const Ask = () => {
onChange={handleTitleChange}
placeholder={t('form.fields.title.placeholder')}
autoFocus
onFocus={() => {
setForceType('title');
}}
onBlur={() => {
setForceType('');
}}
/>
<Form.Control.Feedback type="invalid">
@ -351,12 +344,6 @@ const Ask = () => {
<TagSelector
value={formData.tags.value}
onChange={handleTagsChange}
onFocus={() => {
setForceType('tags');
}}
onBlur={() => {
setForceType('');
}}
/>
<Form.Control.Feedback type="invalid">
{formData.tags.errorMsg}
@ -422,38 +409,17 @@ const Ask = () => {
</Form>
</Col>
<Col xxl={3} lg={4} sm={12}>
{focusType === 'title' && (
<Card className="mb-4">
<Card.Header>{t('how_to_ask.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_ask.description')}</Card.Text>
</Card.Body>
</Card>
)}
{focusType === 'content' && (
<Card className="mb-4">
<Card.Header>{t('how_to_format.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_format.description')}</Card.Text>
</Card.Body>
</Card>
)}
{focusType === 'tags' && (
<Card>
<Card.Header>{t('how_to_tag.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_tag.description')}</Card.Text>
<ul className="mb-0">
{Array.from(
t('how_to_tag.tips', { returnObjects: true }) as string[],
).map((item) => {
return <li key={item}>{item}</li>;
})}
</ul>
</Card.Body>
</Card>
)}
<Card className="mb-4">
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt"
dangerouslySetInnerHTML={{
__html: t('description', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>
</Container>

View File

@ -9,6 +9,7 @@ import {
Icon,
Comment,
FormatTime,
htmlRender,
} from '@answer/components';
import { acceptanceAnswer } from '@answer/api';
import { scrollTop } from '@answer/utils';
@ -44,13 +45,17 @@ const Index: FC<Props> = ({
};
useEffect(() => {
if (aid === data.id && answerRef?.current) {
if (!answerRef?.current) {
return;
}
if (aid === data.id) {
setTimeout(() => {
const element = answerRef.current;
scrollTop(element);
}, 100);
}
}, [data.id, answerRef]);
htmlRender(answerRef.current.querySelector('.fmt'));
}, [data.id, answerRef.current]);
if (!data?.id) {
return null;
}

View File

@ -1,4 +1,4 @@
import { memo, FC, useState, useEffect } from 'react';
import { memo, FC, useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Row, Col, Button } from 'react-bootstrap';
@ -10,6 +10,7 @@ import {
UserCard,
Comment,
FormatTime,
htmlRender,
} from '@answer/components';
import { formatCount } from '@answer/utils';
import { following } from '@answer/api';
@ -25,6 +26,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
keyPrefix: 'question_detail',
});
const [followed, setFollowed] = useState(data?.is_followed);
const ref = useRef<HTMLDivElement>(null);
const handleFollow = (e) => {
e.preventDefault();
@ -42,6 +44,14 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
}
}, [data]);
useEffect(() => {
if (!ref.current) {
return;
}
htmlRender(ref.current);
}, [ref.current]);
if (!data?.id) {
return null;
}
@ -56,14 +66,6 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
</Link>
</h1>
<div className="d-flex align-items-center fs-14 mb-3 text-secondary">
<Button
variant="link"
size="sm"
className="p-0 me-3 btn-no-border"
onClick={(e) => handleFollow(e)}>
{followed ? 'Following' : 'Follow'}
</Button>
<FormatTime
time={data.create_time}
preFix={t('Asked')}
@ -76,10 +78,17 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
className="me-3"
/>
{data?.view_count > 0 && (
<div>
<div className="me-3">
{t('Views')} {formatCount(data.view_count)}
</div>
)}
<Button
variant="link"
size="sm"
className="p-0 btn-no-border"
onClick={(e) => handleFollow(e)}>
{followed ? 'Following' : 'Follow'}
</Button>
</div>
<div className="mb-3">
{data?.tags?.map((item: any) => {
@ -94,6 +103,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
})}
</div>
<article
ref={ref}
dangerouslySetInnerHTML={{ __html: data?.html }}
className="fmt text-break text-wrap"
/>

View File

@ -1,12 +1,3 @@
.fmt {
img {
max-width: 100%;
}
p {
word-break: break-all;
}
}
.answer-item {
border-top: 1px solid rgba(33, 37, 41, 0.25);
}

View File

@ -38,7 +38,6 @@ const Ask = () => {
const [focusType, setForceType] = useState('');
const { t } = useTranslation('translation', { keyPrefix: 'edit_answer' });
const { t: t2 } = useTranslation('translation', { keyPrefix: 'dates' });
const navigate = useNavigate();
const { data } = useQueryAnswerInfo(aid);
@ -154,7 +153,7 @@ const Ask = () => {
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason }, index) => {
const date = dayjs(create_at * 1000).format(
t2('long_date_with_time'),
t('long_date_with_time', { keyPrefix: 'dates' }),
);
return (
<option key={`${create_at}`} value={index}>
@ -217,30 +216,16 @@ const Ask = () => {
</Form>
</Col>
<Col xxl={3} lg={4} sm={12}>
<Card className="mb-4">
<Card.Header>{t('how_to_ask.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_ask.description')}</Card.Text>
</Card.Body>
</Card>
<Card className="mb-4">
<Card.Header>{t('how_to_format.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_format.description')}</Card.Text>
</Card.Body>
</Card>
<Card>
<Card.Header>{t('how_to_tag.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_tag.description')}</Card.Text>
<ul className="mb-0">
{Array.from(
t('how_to_tag.tips', { returnObjects: true }) as string[],
).map((item) => {
return <li>{item}</li>;
})}
</ul>
</Card.Body>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt"
dangerouslySetInnerHTML={{
__html: t('description', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>

View File

@ -45,7 +45,6 @@ const Ask = () => {
const { tagId } = useParams();
const navigate = useNavigate();
const { t } = useTranslation('translation', { keyPrefix: 'edit_tag' });
const { t: t2 } = useTranslation('translation', { keyPrefix: 'dates' });
const [focusType, setForceType] = useState('');
const { data } = useTagInfo({ id: tagId });
@ -159,7 +158,7 @@ const Ask = () => {
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason }, index) => {
const date = dayjs(create_at * 1000).format(
t2('long_date_with_time'),
t('long_date_with_time', { keyPrefix: 'dates' }),
);
return (
<option key={`${create_at}`} value={index}>
@ -249,11 +248,16 @@ const Ask = () => {
</Form>
</Col>
<Col xxl={3} lg={4} sm={12}>
<Card className="mb-4">
<Card.Header>{t('how_to_format.title')}</Card.Header>
<Card.Body>
<Card.Text>{t('how_to_format.description')}</Card.Text>
</Card.Body>
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt"
dangerouslySetInnerHTML={{
__html: t('description', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>