Merge pull request #202 from answerdev/feat/1.0.5/ui

Feat/1.0.5/UI
This commit is contained in:
haitao.jarvis 2023-02-14 18:06:30 +08:00 committed by GitHub
commit bf9ee2a4d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 206 additions and 100 deletions

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"github.copilot"
]
}

View File

@ -326,7 +326,7 @@ ui:
msg:
empty: Code cannot be empty.
language:
label: Language (optional)
label: Language
placeholder: Automatic detection
btn_cancel: Cancel
btn_confirm: Add
@ -362,7 +362,7 @@ ui:
only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB.
desc:
label: Description (optional)
label: Description
tab_url: Image URL
form_url:
fields:
@ -371,7 +371,7 @@ ui:
msg:
empty: Image URL cannot be empty.
name:
label: Description (optional)
label: Description
btn_cancel: Cancel
btn_confirm: Add
uploading: Uploading
@ -391,7 +391,7 @@ ui:
msg:
empty: URL cannot be empty.
name:
label: Description (optional)
label: Description
btn_cancel: Cancel
btn_confirm: Add
ordered_list:
@ -439,7 +439,7 @@ ui:
range: URL slug up to 35 characters.
character: URL slug contains unallowed character set.
desc:
label: Description (optional)
label: Description
btn_cancel: Cancel
btn_submit: Submit
tag_info:
@ -456,10 +456,13 @@ ui:
synonyms_text: The following tags will be remapped to
delete:
title: Delete this tag
content: >-
<p>We do not allowed deleting tag with posts.</p><p>Please remove this tag
from the posts first.</p>
content2: Are you sure you wish to delete?
tip_with_posts: >-
<p>We do not allowed <strong>deleting tag with posts</strong>.</p>
<p>Please remove this tag from the posts first.</p>
tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p>
<p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close
edit_tag:
title: Edit Tag
@ -707,13 +710,13 @@ ui:
default: System
msg: Please upload an avatar
bio:
label: About Me (optional)
label: About Me
website:
label: Website (optional)
label: Website
placeholder: "https://example.com"
msg: Website incorrect format
location:
label: Location (optional)
label: Location
placeholder: "City, Country"
notification:
heading: Notifications
@ -1210,11 +1213,11 @@ ui:
validate: Please enter a valid URL.
text: The address of your site.
short_desc:
label: Short Site Description (optional)
label: Short Site Description
msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage."
desc:
label: Site Description (optional)
label: Site Description
msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag."
contact_email:
@ -1224,14 +1227,6 @@ ui:
text: Email address of key contact responsible for this site.
interface:
page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language:
label: Interface Language
msg: Interface language cannot be empty.
@ -1283,18 +1278,18 @@ ui:
branding:
page_title: Branding
logo:
label: Logo (optional)
label: Logo
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
label: Mobile Logo
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used.
square_icon:
label: Square Icon (optional)
label: Square Icon
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:
label: Favicon (optional)
label: Favicon
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used.
legal:
page_title: Legal
@ -1361,6 +1356,7 @@ ui:
text: Only logged in users can access this community.
form:
optional: (optional)
empty: cannot be empty
invalid: is invalid
btn_submit: Save

View File

@ -16,6 +16,10 @@
#spin-mask {
display: none !important;
}
#protect-brower {
display: none;
}
</style>
</noscript>
<style>
@ -49,11 +53,89 @@
border-radius: 50%;
animation: 0.75s linear infinite _doc-spin;
}
#protect-brower {
padding: 20px;
text-align: center;
}
</style>
<div id="spin-container">
<div class="spinner"></div>
</div>
<div id="protect-brower"></div>
</div>
</div>
</body>
<script>
/**
* @description: Prompt that the browser version is too low
*/
const defaultList = [
{
name: 'Edge',
version: '100'
},
{
name: 'Firefox',
version: '100'
},
{
name: 'Chrome',
version: '90'
},
{
name: 'Safari',
version: '15'
}
];
function getBrowerTypeAndVersion(){
var brower = {
name: '',
version: ''
};
var ua = navigator.userAgent.toLowerCase();
var s;
(s = ua.match(/edge\/([\d\.]+)/)) ? brower = { name: 'Edge', version: s[1] } :
(s = ua.match(/firefox\/([\d\.]+)/)) ? brower = { name: 'Firefox', version: s[1] } :
(s = ua.match(/chrome\/([\d\.]+)/)) ? brower = { name: 'Chrome', version: s[1] } :
(s = ua.match(/version\/([\d\.]+).*safari/)) ? brower = { name: 'Safari', version: s[1] } : brower = { name: 'unknown', version: '' };
// 根据关系进行判断
return brower;
}
function compareVersion(version1, version2) {
var v1 = version1.split('.');
var v2 = version2.split('.');
var len = Math.max(v1.length, v2.length);
while (v1.length < len) {
v1.push('0');
}
while (v2.length < len) {
v2.push('0');
}
for (var i = 0; i < len; i++) {
var num1 = parseInt(v1[i]);
var num2 = parseInt(v2[i]);
if (num1 >= num2) {
return 1;
} else if (num1 < num2) {
return -1;
}
}
return 0;
}
const browerInfo = getBrowerTypeAndVersion();
const notSupport = defaultList.some(item => {
if (item.name === browerInfo.name) {
return compareVersion(browerInfo.version, item.version) === -1;
}
return false;
});
if (notSupport) {
const div = document.getElementById('protect-brower');
div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'
}
</script>
</html>

View File

@ -239,7 +239,12 @@ const Code: FC<IEditorContext> = ({ editor, wrapText }) => {
)}
</Form.Group>
<Form.Group controlId="editor.codeLanguageType" className="mb-3">
<Form.Label>{t('code.form.fields.language.label')}</Form.Label>
<Form.Label>{`${t('code.form.fields.language.label')} ${t(
'optional',
{
keyPrefix: 'form',
},
)}`}</Form.Label>
<Select
options={codeLanguageType}
value={lang}

View File

@ -41,7 +41,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
if (filteredFiles.length > 0) {
AnswerModal.confirm({
content: t('image.only_image'),
content: t('image.form_image.fields.file.msg.only_image'),
});
return false;
}
@ -51,7 +51,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
if (filteredImages.length > 0) {
AnswerModal.confirm({
content: t('image.max_size'),
content: t('image.form_image.fields.file.msg.max_size'),
});
return false;
}
@ -96,13 +96,24 @@ const Image: FC<IEditorContext> = ({ editor }) => {
const endPos = { ...startPos, ch: startPos.ch + loadingText.length };
editor.replaceSelection(loadingText);
const urls = await upload(fileList);
const text = urls.map(({ name, url }) => {
return `![${name}](${url})`;
const urls = await upload(fileList).catch((ex) => {
console.log('ex: ', ex);
});
editor.replaceRange(text.join('\n'), startPos, endPos);
const text: string[] = [];
if (Array.isArray(urls)) {
urls.forEach(({ name, url }) => {
if (name && url) {
text.push(`![${name}](${url})`);
}
});
}
if (text.length) {
editor.replaceRange(text.join('\n'), startPos, endPos);
} else {
// Clear loading text
editor.replaceRange('', startPos, endPos);
}
};
const paste = async (_, event) => {
@ -251,7 +262,12 @@ const Image: FC<IEditorContext> = ({ editor }) => {
<Form.Group controlId="editor.imgDescription" className="mb-3">
<Form.Label>
{t('image.form_image.fields.desc.label')}
{`${t('image.form_image.fields.desc.label')} ${t(
'optional',
{
keyPrefix: 'form',
},
)}`}
</Form.Label>
<Form.Control
type="text"
@ -285,7 +301,9 @@ const Image: FC<IEditorContext> = ({ editor }) => {
<Form.Group controlId="editor.imgName" className="mb-3">
<Form.Label>
{t('image.form_url.fields.name.label')}
{`${t('image.form_url.fields.name.label')} ${t('optional', {
keyPrefix: 'form',
})}`}
</Form.Label>
<Form.Control
type="text"

View File

@ -88,7 +88,12 @@ const Link: FC<IEditorContext> = ({ editor }) => {
</Form.Group>
<Form.Group controlId="editor.internetSiteName" className="mb-3">
<Form.Label>{t('link.form.fields.name.label')}</Form.Label>
<Form.Label>{`${t('link.form.fields.name.label')} ${t(
'optional',
{
keyPrefix: 'form',
},
)}`}</Form.Label>
<Form.Control
type="text"
value={name.value}

View File

@ -225,7 +225,9 @@ const useTagModal = (props: IProps = {}) => {
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="description">
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
<Form.Label>{`${t('form.fields.desc.label')} ${t('optional', {
keyPrefix: 'form',
})}`}</Form.Label>
<Form.Control
className="font-monospace"
value={formData.description.value}

View File

@ -44,22 +44,28 @@ const Index: FC = () => {
properties: {
logo: {
type: 'string',
title: t('logo.label'),
title: `${t('logo.label')} ${t('optional', { keyPrefix: 'form' })}`,
description: t('logo.text'),
},
mobile_logo: {
type: 'string',
title: t('mobile_logo.label'),
title: `${t('mobile_logo.label')} ${t('optional', {
keyPrefix: 'form',
})}`,
description: t('mobile_logo.text'),
},
square_icon: {
type: 'string',
title: t('square_icon.label'),
title: `${t('square_icon.label')} ${t('optional', {
keyPrefix: 'form',
})}`,
description: t('square_icon.text'),
},
favicon: {
type: 'string',
title: t('favicon.label'),
title: `${t('favicon.label')} ${t('optional', {
keyPrefix: 'form',
})}`,
description: t('favicon.text'),
},
},

View File

@ -33,12 +33,16 @@ const General: FC = () => {
},
short_description: {
type: 'string',
title: t('short_desc.label'),
title: `${t('short_desc.label')} ${t('optional', {
keyPrefix: 'form',
})}`,
description: t('short_desc.text'),
},
description: {
type: 'string',
title: t('desc.label'),
title: `${t('desc.label')} ${t('optional', {
keyPrefix: 'form',
})}`,
description: t('desc.text'),
},
contact_email: {

View File

@ -110,25 +110,25 @@ const Ask = () => {
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
title: { ...formData.title, value: e.currentTarget.value },
title: { ...formData.title, value: e.currentTarget.value, errorMsg: '' },
});
};
const handleContentChange = (value: string) => {
setFormData({
...formData,
content: { ...formData.content, value },
content: { ...formData.content, value, errorMsg: '' },
});
};
const handleTagsChange = (value) =>
setFormData({
...formData,
tags: { ...formData.tags, value },
tags: { ...formData.tags, value, errorMsg: '' },
});
const handleAnswerChange = (value: string) =>
setFormData({
...formData,
answer: { ...formData.answer, value },
answer: { ...formData.answer, value, errorMsg: '' },
});
const handleSummaryChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
@ -140,55 +140,9 @@ const Ask = () => {
},
});
const checkValidated = (): boolean => {
const bol = true;
const { title, content, tags, answer } = formData;
if (title.value && Array.from(title.value).length <= 150) {
formData.title = {
value: title.value,
isInvalid: false,
errorMsg: '',
};
}
if (content.value) {
formData.content = {
value: content.value,
isInvalid: false,
errorMsg: '',
};
}
if (Array.isArray(tags.value) && tags.value.length > 0) {
formData.tags = {
value: tags.value,
isInvalid: false,
errorMsg: '',
};
}
if (checked) {
if (answer.value) {
formData.answer = {
value: answer.value,
isInvalid: false,
errorMsg: '',
};
}
}
setFormData({
...formData,
});
return bol;
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
if (!checkValidated()) {
return;
}
const params: Type.QuestionParams = {
title: formData.title.value,
@ -232,7 +186,9 @@ const Ask = () => {
})
.catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
const data = handleFormError(err, formData, [
{ from: 'content', to: 'answer' },
]);
setFormData({ ...data });
}
});

View File

@ -86,15 +86,27 @@ const TagIntroduction = () => {
if (synonymsData?.synonyms && synonymsData.synonyms.length > 0) {
Modal.confirm({
title: t('delete.title'),
content: t('delete.content2'),
content: t('delete.tip_with_synonyms'),
showConfirm: false,
cancelText: t('delete.close'),
});
return;
}
if (tagInfo.question_count > 0) {
Modal.confirm({
title: t('delete.title'),
content: t('delete.tip_with_posts'),
showConfirm: false,
cancelText: t('delete.close'),
});
return;
}
Modal.confirm({
title: t('delete.title'),
content: t('delete.content'),
content: t('delete.tip'),
confirmText: t('delete', { keyPrefix: 'btns' }),
confirmBtnVariant: 'danger',
onConfirm: () => {
deleteTag(tagInfo.tag_id);
},

View File

@ -384,7 +384,11 @@ const Index: React.FC = () => {
</Form.Group>
<Form.Group controlId="bio" className="mb-3">
<Form.Label>{t('bio.label')}</Form.Label>
<Form.Label>
{`${t('bio.label')} ${t('optional', {
keyPrefix: 'form',
})}`}
</Form.Label>
<Form.Control
className="font-monospace"
required
@ -408,7 +412,9 @@ const Index: React.FC = () => {
</Form.Group>
<Form.Group controlId="website" className="mb-3">
<Form.Label>{t('website.label')}</Form.Label>
<Form.Label>{`${t('website.label')} ${t('optional', {
keyPrefix: 'form',
})}`}</Form.Label>
<Form.Control
required
type="url"
@ -431,7 +437,9 @@ const Index: React.FC = () => {
</Form.Group>
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('location.label')}</Form.Label>
<Form.Label>{`${t('location.label')} ${t('optional', {
keyPrefix: 'form',
})}`}</Form.Label>
<Form.Control
required
type="text"

View File

@ -168,9 +168,16 @@ function labelStyle(color, hover) {
function handleFormError(
error: { list: Array<{ error_field: string; error_msg: string }> },
data: any,
keymap?: Array<{ from: string; to: string }>,
) {
if (error.list?.length > 0) {
error.list.forEach((item) => {
if (keymap?.length) {
const key = keymap.find((k) => k.from === item.error_field);
if (key) {
item.error_field = key.to;
}
}
const errorFieldObject = data[item.error_field];
if (errorFieldObject) {
errorFieldObject.isInvalid = true;