mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4
This commit is contained in:
commit
feb07b76b1
|
@ -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:
|
||||
|
|
|
@ -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 |
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -159,6 +159,8 @@ const TagIntroduction = () => {
|
|||
slug_name: tagName || '',
|
||||
main_tag_slug_name: '',
|
||||
display_name: '',
|
||||
recommend: false,
|
||||
reserved: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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')} />;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue