Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4

This commit is contained in:
haitao(lj) 2022-11-17 14:42:27 +08:00
commit feb07b76b1
18 changed files with 178 additions and 67 deletions

View File

@ -1134,6 +1134,20 @@ ui:
"no": "No"
branding:
page_title: Branding
logo:
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)
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
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)
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
terms_of_service:

View File

@ -2,7 +2,6 @@
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -24,6 +24,8 @@ export interface ReportParams {
export interface TagBase {
display_name: string;
slug_name: string;
recommend: boolean;
reserved: boolean;
}
export interface Tag extends TagBase {
@ -305,8 +307,9 @@ export interface AdminSettingsLegal {
}
export interface AdminSettingsWrite {
recommend_tags: string;
recommend_tags: string[];
required_tag: string;
reserved_tags: string[];
}
/**

View File

@ -74,11 +74,19 @@ const Header: FC = () => {
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap">
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3">
{brandingInfo.logo ? (
<img
className="logo rounded-1 me-0"
src={brandingInfo.logo}
alt=""
/>
<>
<img
className="d-none d-lg-block logo rounded-1 me-0"
src={brandingInfo.logo}
alt=""
/>
<img
className="logo d-block d-lg-none rounded-1 me-0"
src={brandingInfo.mobile_logo || brandingInfo.mobile_logo}
alt=""
/>
</>
) : (
<span>{siteInfo.name || 'Answer'}</span>
)}

View File

@ -99,7 +99,21 @@ const SchemaForm: FC<IProps> = ({
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const data = { ...formData, [name]: { ...formData[name], value } };
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value: checked, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
@ -163,6 +177,7 @@ const SchemaForm: FC<IProps> = ({
const errors = requiredValidator();
if (errors.length > 0) {
formData = errors.reduce((acc, cur) => {
console.log('schema.properties[cur]', cur);
acc[cur] = {
...formData[cur],
isInvalid: true,
@ -270,17 +285,21 @@ const SchemaForm: FC<IProps> = ({
}
if (widget === 'switch') {
console.log(formData[key]?.value, 'switch=====');
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Label>{title}</Form.Label>
<Form.Check
required
id={title}
name={key}
type="switch"
label={title}
checked={formData[key]?.value}
feedback={formData[key]?.errorMsg}
feedbackType="invalid"
isInvalid={formData[key].isInvalid}
onChange={handleSwitchChange}
/>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}

View File

@ -14,7 +14,14 @@ const Index: FC<IProps> = ({ className = '', href, data }) => {
href =
href || `/tags/${data.main_tag_slug_name || data.slug_name}`.toLowerCase();
return (
<a href={href} className={classNames('badge-tag rounded-1', className)}>
<a
href={href}
className={classNames(
'badge-tag rounded-1',
data.reserved && 'badge-tag-reserved',
data.recommend && 'badge-tag-required',
className,
)}>
{data.slug_name}
</a>
);

View File

@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { FC, useState, useEffect } from 'react';
import { Dropdown, FormControl, Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
@ -95,17 +96,16 @@ const TagSelector: FC<IProps> = ({
}
}, [value]);
useEffect(() => {
if (!tag) {
setTags(null);
return;
}
queryTags(tag).then((res) => {
const fetchTags = (str) => {
queryTags(str).then((res) => {
const tagArray: Type.Tag[] = filterTags(res || []);
setTags(tagArray);
});
}, [tag]);
};
useEffect(() => {
fetchTags(tag);
}, [visibleMenu]);
const handleClick = (val: Type.Tag) => {
const findIndex = initialValue.findIndex(
@ -143,7 +143,9 @@ const TagSelector: FC<IProps> = ({
};
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
setTag(e.currentTarget.value.replace(';', ''));
const searchStr = e.currentTarget.value.replace(';', '');
setTag(searchStr);
fetchTags(searchStr);
};
const handleSelect = (eventKey) => {
@ -186,7 +188,9 @@ const TagSelector: FC<IProps> = ({
'm-1 text-nowrap d-flex align-items-center',
index === repeatIndex && 'warning',
)}
variant="outline-secondary"
variant={`outline-${
item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary'
}`}
size="sm">
{item.slug_name}
<span className="ms-1" onMouseUp={() => handleRemove(item)}>
@ -220,6 +224,14 @@ const TagSelector: FC<IProps> = ({
</Form>
</Dropdown.Header>
)}
{tags && tags.filter((v) => v.recommend)?.length > 0 && (
<Dropdown.Item
disabled
style={{ fontWeight: 500 }}
className="text-secondary">
Required tag (at least one)
</Dropdown.Item>
)}
{tags?.map((item, index) => {
return (

View File

@ -69,17 +69,29 @@ a {
padding: 1px 0.5rem 2px;
color: $blue-700;
height: 24px;
border: 1px solid rgba($blue-100, 0.5);
&:hover {
background: rgba($blue-100, 1);
}
}
.badge-tag-required {
background: rgba($gray-400, 0.5);
background: rgba($gray-200, 0.5);
color: $gray-700;
border: 1px solid $gray-400;
&:hover {
color: $gray-700;
background: rgba($gray-400, 1);
background: rgba($gray-200, 1);
}
}
.badge-tag-reserved {
background: rgba($orange-100, 0.5);
color: $orange-700;
border: 1px solid $orange-400;
&:hover {
color: $orange-700;
background: rgba($orange-100, 1);
}
}

View File

@ -22,17 +22,36 @@ const HealthStatus: FC<IProps> = ({ data }) => {
<span className="text-secondary me-1">{t('version')}</span>
<strong>{version}</strong>
{isLatest && (
<Badge pill bg="success" className="ms-1">
<Badge
pill
bg="success"
className="ms-1"
as="a"
target="_blank"
href="https://github.com/answerdev/answer/releases">
{t('latest')}
</Badge>
)}
{!isLatest && remote_version && (
<Badge pill bg="warning" text="dark" className="ms-1">
<Badge
pill
bg="warning"
text="dark"
className="ms-1"
as="a"
target="_blank"
href="https://github.com/answerdev/answer/releases">
{t('update_to')} {remote_version}
</Badge>
)}
{!isLatest && !remote_version && (
<Badge pill bg="danger" className="ms-1">
<Badge
pill
bg="danger"
className="ms-1"
as="a"
target="_blank"
href="https://github.com/answerdev/answer/releases">
{t('check_failed')}
</Badge>
)}

View File

@ -3,9 +3,11 @@ import { useTranslation } from 'react-i18next';
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
import type * as Type from '@/common/interface';
// import { useToast } from '@/hooks';
// import { siteInfoStore } from '@/stores';
import { useGeneralSetting } from '@/services';
import { useToast } from '@/hooks';
import {
getRequireAndReservedTag,
postRequireAndReservedTag,
} from '@/services';
import '../index.scss';
@ -13,13 +15,11 @@ const Legal: FC = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'admin.write',
});
// const Toast = useToast();
const Toast = useToast();
// const updateSiteInfo = siteInfoStore((state) => state.update);
const { data: setting } = useGeneralSetting();
const schema: JSONSchema = {
title: t('page_title'),
required: ['terms_of_service', 'privacy_policy'],
properties: {
recommend_tags: {
type: 'string',
@ -62,38 +62,40 @@ const Legal: FC = () => {
evt.stopPropagation();
const reqParams: Type.AdminSettingsWrite = {
recommend_tags: formData.recommend_tags.value,
recommend_tags: formData.recommend_tags.value.trim().split('\n'),
required_tag: formData.required_tag.value,
reserved_tags: formData.reserved_tags.value.trim().split('\n'),
};
console.log(reqParams);
// updateGeneralSetting(reqParams)
// .then(() => {
// Toast.onShow({
// msg: t('update', { keyPrefix: 'toast' }),
// variant: 'success',
// });
// updateSiteInfo(reqParams);
// })
// .catch((err) => {
// if (err.isError && err.key) {
// formData[err.key].isInvalid = true;
// formData[err.key].errorMsg = err.value;
// }
// setFormData({ ...formData });
// });
postRequireAndReservedTag(reqParams)
.then(() => {
Toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
})
.catch((err) => {
if (err.isError && err.key) {
formData[err.key].isInvalid = true;
formData[err.key].errorMsg = err.value;
}
setFormData({ ...formData });
});
};
const initData = () => {
getRequireAndReservedTag().then((res) => {
formData.recommend_tags.value = res.recommend_tags.join('\n');
formData.required_tag.value = res.required_tag;
formData.reserved_tags.value = res.reserved_tags.join('\n');
setFormData({ ...formData });
});
};
useEffect(() => {
if (!setting) {
return;
}
const formMeta = {};
Object.keys(setting).forEach((k) => {
formMeta[k] = { ...formData[k], value: setting[k] };
});
setFormData({ ...formData, ...formMeta });
}, [setting]);
initData();
}, []);
const handleOnChange = (data) => {
setFormData(data);

View File

@ -4,12 +4,13 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr';
import { siteInfoStore, toastStore } from '@/stores';
import { siteInfoStore, toastStore, brandingStore } from '@/stores';
import { Header, Footer, Toast } from '@/components';
const Layout: FC = () => {
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
const { siteInfo } = siteInfoStore.getState();
const { favicon } = brandingStore((state) => state.branding);
const closeToast = () => {
toastClear();
};
@ -17,6 +18,7 @@ const Layout: FC = () => {
return (
<HelmetProvider>
<Helmet>
<link rel="icon" href={favicon || '/favicon.ico'} />
{siteInfo && <meta name="description" content={siteInfo.description} />}
</Helmet>
<SWRConfig

View File

@ -36,7 +36,7 @@ const Index: FC<Props> = ({ data }) => {
<div className="mb-5">
<h3 className="mb-3">{t('title')}</h3>
<p>
<span className="me-1 text-secondary">{t('keywords')}</span>
<span className="text-secondary">{t('keywords')}</span>
{q?.replace(reg, '')}
<br />
{options?.length && (

View File

@ -159,6 +159,8 @@ const TagIntroduction = () => {
slug_name: tagName || '',
main_tag_slug_name: '',
display_name: '',
recommend: false,
reserved: false,
}}
/>
</div>

View File

@ -1,6 +1,6 @@
import { FC, memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { loggedUserInfoStore } from '@/stores';
import { activateAccount } from '@/services';
@ -10,6 +10,7 @@ const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
const [searchParams] = useSearchParams();
const updateUser = loggedUserInfoStore((state) => state.update);
const navigate = useNavigate();
useEffect(() => {
const code = searchParams.get('code');
@ -17,9 +18,11 @@ const Index: FC = () => {
activateAccount(encodeURIComponent(code)).then((res) => {
updateUser(res);
setTimeout(() => {
window.location.replace('/users/account-activation/success');
navigate('/users/account-activation/success', { replace: true });
}, 0);
});
} else {
navigate('/', { replace: true });
}
}, []);
return <PageTitle title={t('account_activation')} />;

View File

@ -167,13 +167,13 @@ const routes: RouteNode[] = [
{
path: 'users/account-activation',
page: 'pages/Users/ActiveEmail',
guard: async () => {
const notActivated = guard.notActivated();
if (notActivated.ok) {
return notActivated;
}
return guard.notLogged();
},
// guard: async () => {
// const notActivated = guard.notActivated();
// if (notActivated.ok) {
// return notActivated;
// }
// return guard.notLogged();
// },
},
{
path: 'users/account-activation/success',

View File

@ -84,6 +84,14 @@ export const brandSetting = (params: Type.AdmingSettingBranding) => {
return request.put('/answer/admin/api/siteinfo/branding', params);
};
export const getRequireAndReservedTag = () => {
return request.get('/answer/admin/api/siteinfo/write');
};
export const postRequireAndReservedTag = (params) => {
return request.put('/answer/admin/api/siteinfo/write', params);
};
export const getLegalSetting = () => {
return request.get<Type.AdminSettingsLegal>(
'/answer/admin/api/siteinfo/legal',

View File

@ -49,7 +49,7 @@ class Request {
},
(error) => {
const { status, data: respData, msg: respMsg } = error.response || {};
const { data, msg = '' } = respData;
const { data = {}, msg = '' } = respData || {};
if (status === 400) {
// show error message
if (data instanceof Object && data.err_type) {
@ -103,6 +103,7 @@ class Request {
return Promise.reject(false);
}
if (status === 403) {
debugger;
// Permission interception
if (data?.type === 'url_expired') {
// url expired