chore: admin/general,interface,seo, customize etc...

This commit is contained in:
haitao(lj) 2022-12-14 15:08:52 +08:00
parent 56bb48b80b
commit 30f7edd614
25 changed files with 321 additions and 140 deletions

View File

@ -1165,9 +1165,6 @@ ui:
msg: Contact email cannot be empty.
validate: Contact email is not valid.
text: Email address of key contact responsible for this site.
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
interface:
page_title: Interface
logo:
@ -1264,6 +1261,9 @@ ui:
text: "Reserved tags can only be added to a post by moderator."
seo:
page_title: SEO
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
robots:
label: robots.txt
text: This will permanently override any related site settings.

View File

@ -18,6 +18,7 @@
"bootstrap-icons": "1.10.2",
"classnames": "^2.3.1",
"codemirror": "5.65.0",
"color": "^4.2.3",
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"diff": "^5.1.0",

View File

@ -22,6 +22,7 @@ specifiers:
bootstrap-icons: 1.10.2
classnames: ^2.3.1
codemirror: 5.65.0
color: ^4.2.3
copy-to-clipboard: ^3.3.2
customize-cra: ^1.0.0
dayjs: ^1.11.5
@ -74,6 +75,7 @@ dependencies:
bootstrap-icons: 1.10.2
classnames: 2.3.2
codemirror: 5.65.0
color: 4.2.3
copy-to-clipboard: 3.3.2
dayjs: 1.11.5
diff: 5.1.0
@ -3679,6 +3681,21 @@ packages:
/color-name/1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
/color-string/1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: false
/color/4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
dev: false
/colord/2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@ -6294,6 +6311,10 @@ packages:
/is-arrayish/0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
/is-arrayish/0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@ -9615,6 +9636,12 @@ packages:
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
/simple-swizzle/0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: false
/sisteransi/1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}

View File

@ -279,12 +279,6 @@ export interface AdminSettingsGeneral {
description: string;
site_url: string;
contact_email: string;
/**
* 0: not set
* 1with title
* 2: no title
*/
permalink: number;
}
export interface HelmetBase {
@ -300,7 +294,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
export interface AdminSettingsInterface {
language: string;
theme: string;
time_zone?: string;
}
@ -322,6 +315,8 @@ export interface SiteSettings {
interface: AdminSettingsInterface;
login: AdminSettingsLogin;
custom_css_html: AdminSettingsCustom;
theme: AdminSettingsTheme;
site_seo: AdminSettingsSeo;
}
export interface AdminSettingBranding {
@ -346,6 +341,12 @@ export interface AdminSettingsWrite {
export interface AdminSettingsSeo {
robots: string;
/**
* 0: not set
* 1with title
* 2: no title
*/
permalink: number;
}
export type themeConfig = {
@ -355,6 +356,7 @@ export type themeConfig = {
};
export interface AdminSettingsTheme {
theme: string;
theme_options?: { label: string; value: string }[];
theme_config: Record<string, themeConfig>;
}

View File

@ -0,0 +1,76 @@
import { FC } from 'react';
import { Helmet } from 'react-helmet-async';
import Color from 'color';
import { shiftColor, tintColor, shadeColor } from '@/utils';
import { themeSettingStore } from '@/stores';
const Index: FC = () => {
const { theme, theme_config } = themeSettingStore((_) => _);
let primaryColor;
if (theme_config[theme]?.primary_color) {
primaryColor = Color(theme_config[theme].primary_color);
}
return (
<Helmet>
{primaryColor && (
<style>
{`
:root {
--bs-blue: ${primaryColor.hex()};
--bs-primary: ${primaryColor.hex()};
--bs-primary-rgb: ${primaryColor.rgb().array().join(',')};
--bs-link-color: ${primaryColor.hex()};
--bs-link-hover-color: ${shiftColor(primaryColor, 0.8)};
}
.nav-pills {
--bs-nav-pills-link-active-bg: ${primaryColor.hex()};
}
.btn-primary {
--bs-btn-bg: ${primaryColor.hex()};
--bs-btn-border-color: ${primaryColor.hex()};
--bs-btn-hover-bg: ${tintColor(primaryColor, 0.85)};
--bs-btn-hover-border-color: ${tintColor(primaryColor, 0.9)};
--bs-btn-focus-shadow-rgb: ${shadeColor(primaryColor, 0.85)};
--bs-btn-active-bg: ${tintColor(primaryColor, 0.8)};
--bs-btn-active-border-color: ${tintColor(primaryColor, 0.9)};
--bs-btn-disabled-bg: ${primaryColor.hex()};
--bs-btn-disabled-border-color: ${primaryColor.hex()};
}
.btn-outline-primary {
--bs-btn-color: ${primaryColor.hex()};
--bs-btn-border-color: ${primaryColor.hex()};
--bs-btn-hover-bg: ${primaryColor.hex()};
--bs-btn-hover-border-color: ${primaryColor.hex()};
--bs-btn-active-bg: ${primaryColor.hex()};
--bs-btn-active-border-color: ${primaryColor.hex()};
--bs-btn-disabled-color: ${primaryColor.hex()};
--bs-btn-disabled-border-color: ${primaryColor.hex()};
}
.pagination {
--bs-btn-color: ${primaryColor.hex()};
--bs-pagination-active-bg: ${primaryColor.hex()};
--bs-pagination-active-border-color: ${primaryColor.hex()};
}
.form-select:focus,
.form-control:focus {
box-shadow: 0 0 0 0.25rem ${primaryColor.fade(0.75).string()};
border-color: ${tintColor(primaryColor, 0.5)};
}
.badge-tag:not(.badge-tag-reserved, .badge-tag-required) {
color: ${shadeColor(primaryColor, 0.4)};
background: ${tintColor(primaryColor, 0.8).fade(0.5).string()};
}
.badge-tag:hover:not(.badge-tag-reserved, .badge-tag-required) {
background: ${tintColor(primaryColor, 0.8).hex()};
}
`}
</style>
)}
</Helmet>
);
};
export default Index;

View File

@ -20,9 +20,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
as={NavLink}
to="/users/notifications/inbox"
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
<div className="text-white text-opacity-75">
<Icon name="bell-fill" className="fs-4" />
</div>
<Icon name="bell-fill" className="fs-4" />
{(redDot?.inbox || 0) > 0 && <div className="unread-dot bg-danger" />}
</Nav.Link>
@ -30,9 +28,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
as={Link}
to="/users/notifications/achievement"
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
<div className="text-white text-opacity-75">
<Icon name="trophy-fill" className="fs-4" />
</div>
<Icon name="trophy-fill" className="fs-4" />
{(redDot?.achievement || 0) > 0 && (
<div className="unread-dot bg-danger" />
)}

View File

@ -1,32 +1,20 @@
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
#header {
background: linear-gradient(180deg, #0033FF 0%, rgba(0, 51, 255, 0.95) 100%);
--bs-navbar-padding-y: 0.75rem;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15), $box-shadow-sm;
.logo {
max-height: 1.75rem;
}
.nav-link {
color: rgba(255, 255, 255, 0.7);
&.active {
font-weight: bold;
color: #fff;
}
&.icon-link {
width: 36px;
height: 36px;
}
}
.placeholder-search {
background-color: rgba(255, 255, 255, .2);
border: $border-width $border-style rgba(255, 255, 255, .2);
&:focus {
border: $border-width $border-style $border-color;
}
&::placeholder {
color: rgba(255, 255, 255, 0.75);
}
box-shadow: none;
}
.answer-navBar {
@ -45,6 +33,53 @@
.hr {
color: #fff;
}
// style for colored navbar
&.theme-colored {
background: linear-gradient(180deg, #0033FF 0%, rgba(0, 51, 255, 0.95) 100%);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15), $box-shadow-sm;
.nav-link {
color: rgba(255, 255, 255, 0.7);
&.active {
font-weight: bold;
color: #fff;
}
}
.placeholder-search {
color: #fff;
background-color: rgba(255, 255, 255, .2);
border: $border-width $border-style rgba(255, 255, 255, .2);
&:focus {
border: $border-width $border-style $border-color;
}
&::placeholder {
color: rgba(255, 255, 255, 0.75);
}
}
}
// style for light navbar
&.theme-light {
background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0.95) 100%);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 0.125rem 0.25rem rgb(0 0 0 / 8%);
.nav-link {
color: rgba(0, 0, 0, 0.55);
&.active {
font-weight: bold;
color: rgba(0, 0, 0, 0.75)
}
}
.placeholder-search {
color: rgba(0, 0, 0, 0.75);
background-color: #fff;
border: 1px solid #CED4DA;
&:focus {
border: 1px solid rgba(0, 0, 0, 0.75);
}
&::placeholder {
color: #6C757D
}
}
}
}
@ -52,7 +87,7 @@
#header {
.logo {
max-width: 93px;
max-height: auto;
max-height: initial;
}
.nav-grow {
flex-grow: 1!important;
@ -73,7 +108,7 @@
#header {
.logo {
max-width: 93px;
max-height: auto;
max-height: initial;
}
}
}

View File

@ -17,11 +17,14 @@ import {
useLocation,
} from 'react-router-dom';
import classnames from 'classnames';
import {
loggedUserInfoStore,
siteInfoStore,
brandingStore,
loginSettingStore,
themeSettingStore,
} from '@/stores';
import { logout, useQueryNotificationStatus } from '@/services';
import { RouteAlias } from '@/router/alias';
@ -41,6 +44,7 @@ const Header: FC = () => {
const siteInfo = siteInfoStore((state) => state.siteInfo);
const brandingInfo = brandingStore((state) => state.branding);
const loginSetting = loginSettingStore((state) => state.login);
const { theme, theme_config } = themeSettingStore((_) => _);
const { data: redDot } = useQueryNotificationStatus();
const location = useLocation();
const handleInput = (val) => {
@ -69,8 +73,16 @@ const Header: FC = () => {
}
}, [location.pathname]);
let themeType = 'theme-colored';
if (theme && theme_config[theme]) {
themeType = `theme-${theme_config[theme].navbar_style}`;
}
return (
<Navbar variant="dark" expand="lg" className="sticky-top" id="header">
<Navbar
expand="lg"
className={classnames('sticky-top', themeType)}
id="header">
<Container className="d-flex align-items-center">
<Navbar.Toggle
aria-controls="navBarContent"
@ -105,14 +117,15 @@ const Header: FC = () => {
<NavItems redDot={redDot} userInfo={user} logOut={handleLogout} />
) : (
<>
<Button
variant="link"
className="me-2 text-white"
href="/users/login">
<Button variant="link" className="me-2" href="/users/login">
{t('btns.login')}
</Button>
{loginSetting.allow_new_registrations && (
<Button variant="light" href="/users/register">
<Button
variant={
themeType === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
{t('btns.signup')}
</Button>
)}
@ -139,7 +152,7 @@ const Header: FC = () => {
<Form action="/search" className="w-75 px-0 px-lg-2">
<FormControl
placeholder={t('header.search.placeholder')}
className="text-white placeholder-search"
className="placeholder-search"
value={searchStr}
name="q"
onChange={(e) => handleInput(e.target.value)}
@ -163,7 +176,10 @@ const Header: FC = () => {
<Nav.Item className="me-3">
<Link
to="/questions/ask"
className="text-capitalize text-nowrap btn btn-light">
className={classnames('text-capitalize text-nowrap btn', {
'btn-light': themeType !== 'theme-light',
'btn-primary': themeType === 'theme-light',
})}>
{t('btns.add_question')}
</Link>
</Nav.Item>
@ -178,12 +194,19 @@ const Header: FC = () => {
<>
<Button
variant="link"
className="me-2 text-white"
className={classnames('me-2', {
'link-light': themeType === 'theme-colored',
'link-primary': themeType !== 'theme-colored',
})}
href="/users/login">
{t('btns.login')}
</Button>
{loginSetting.allow_new_registrations && (
<Button variant="light" href="/users/register">
<Button
variant={
themeType === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
{t('btns.signup')}
</Button>
)}

View File

@ -28,6 +28,7 @@ import SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm';
import Labels from './LabelsCard';
import DiffContent from './DiffContent';
import Customize from './Customize';
import CustomizeTheme from './CustomizeTheme';
export {
Avatar,
@ -62,5 +63,6 @@ export {
Labels,
DiffContent,
Customize,
CustomizeTheme,
};
export type { EditorRef, JSONSchema, UISchema };

View File

@ -66,7 +66,7 @@ a {
display: inline-block;
font-size: 14px;
background: rgba($blue-100, 0.5);
padding: 0px 7px 1px;
padding: 0 7px 1px;
color: $blue-700;
border: 1px solid transparent;
&:hover {

View File

@ -46,14 +46,6 @@ const General: FC = () => {
title: t('contact_email.label'),
description: t('contact_email.text'),
},
permalink: {
type: 'number',
title: t('permalink.label'),
description: t('permalink.text'),
enum: [1, 2],
enumNames: ['/questions/123/post-title', '/questions/123'],
default: 1,
},
},
};
const uiSchema: UISchema = {
@ -92,9 +84,6 @@ const General: FC = () => {
},
},
},
permalink: {
'ui:widget': 'select',
},
};
const [formData, setFormData] = useState<Type.FormDataType>(
initFormData(schema),
@ -108,7 +97,6 @@ const General: FC = () => {
short_description: formData.short_description.value,
site_url: formData.site_url.value,
contact_email: formData.contact_email.value,
permalink: Number(formData.permalink.value),
};
updateGeneralSetting(reqParams)
@ -135,9 +123,6 @@ const General: FC = () => {
Object.keys(formData).forEach((k) => {
formMeta[k] = { ...formData[k], value: setting[k] };
});
if (formMeta.permalink.value !== 1 && formMeta.permalink.value !== 2) {
formMeta.permalink.value = 1;
}
setFormData({ ...formData, ...formMeta });
}, [setting]);

View File

@ -10,11 +10,7 @@ import {
import { interfaceStore } from '@/stores';
import { JSONSchema, SchemaForm, UISchema } from '@/components';
import { DEFAULT_TIMEZONE } from '@/common/constants';
import {
updateInterfaceSetting,
useInterfaceSetting,
useThemeOptions,
} from '@/services';
import { updateInterfaceSetting, useInterfaceSetting } from '@/services';
import {
setupAppLanguage,
loadLanguageOptions,
@ -27,7 +23,6 @@ const Interface: FC = () => {
keyPrefix: 'admin.interface',
});
const storeInterface = interfaceStore.getState().interface;
const { data: themes } = useThemeOptions();
const Toast = useToast();
const [langs, setLangs] = useState<LangsType[]>();
const { data: setting } = useInterfaceSetting();
@ -35,13 +30,6 @@ const Interface: FC = () => {
const schema: JSONSchema = {
title: t('page_title'),
properties: {
theme: {
type: 'string',
title: t('theme.label'),
description: t('theme.text'),
enum: themes?.map((theme) => theme.value) || [],
enumNames: themes?.map((theme) => theme.label) || [],
},
language: {
type: 'string',
title: t('language.label'),
@ -58,11 +46,6 @@ const Interface: FC = () => {
};
const [formData, setFormData] = useState<FormDataType>({
theme: {
value: setting?.theme || storeInterface.theme,
isInvalid: false,
errorMsg: '',
},
language: {
value: setting?.language || storeInterface.language,
isInvalid: false,
@ -76,9 +59,6 @@ const Interface: FC = () => {
});
const uiSchema: UISchema = {
theme: {
'ui:widget': 'select',
},
language: {
'ui:widget': 'select',
},
@ -90,30 +70,11 @@ const Interface: FC = () => {
const res: LangsType[] = await loadLanguageOptions(true);
setLangs(res);
};
// set default theme value
if (!formData.theme.value && Array.isArray(themes) && themes.length) {
setFormData({
...formData,
theme: {
value: themes[0].value,
isInvalid: false,
errorMsg: '',
},
});
}
const checkValidated = (): boolean => {
let ret = true;
const { theme, language } = formData;
const { language } = formData;
const formCheckData = { ...formData };
if (!theme.value) {
ret = false;
formCheckData.theme = {
value: '',
isInvalid: true,
errorMsg: t('theme.msg'),
};
}
if (!language.value) {
ret = false;
formCheckData.language = {
@ -134,7 +95,6 @@ const Interface: FC = () => {
return;
}
const reqParams: AdminSettingsInterface = {
theme: formData.theme.value,
language: formData.language.value,
time_zone: formData.time_zone.value,
};
@ -156,21 +116,6 @@ const Interface: FC = () => {
}
});
};
// const imgUpload = (file: any) => {
// return new Promise((resolve) => {
// uploadAvatar(file).then((res) => {
// setFormData({
// ...formData,
// logo: {
// value: res,
// isInvalid: false,
// errorMsg: '',
// },
// });
// resolve(true);
// });
// });
// };
useEffect(() => {
if (setting) {

View File

@ -6,6 +6,7 @@ import { getLoginSetting, putLoginSetting } from '@/services';
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
import { useToast } from '@/hooks';
import { handleFormError } from '@/utils';
import { loginSettingStore } from '@/stores';
const Index: FC = () => {
const { t } = useTranslation('translation', {
@ -40,6 +41,7 @@ const Index: FC = () => {
},
};
const [formData, setFormData] = useState(initFormData(schema));
const { update: updateLoginSetting } = loginSettingStore((_) => _);
const onSubmit = (evt) => {
evt.preventDefault();
@ -56,6 +58,7 @@ const Index: FC = () => {
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
updateLoginSetting(reqParams);
})
.catch((err) => {
if (err.isError) {

View File

@ -15,6 +15,14 @@ const Index: FC = () => {
const schema: JSONSchema = {
title: t('page_title'),
properties: {
permalink: {
type: 'number',
title: t('permalink.label'),
description: t('permalink.text'),
enum: [1, 2],
enumNames: ['/questions/123/post-title', '/questions/123'],
default: 1,
},
robots: {
type: 'string',
title: t('robots.label'),
@ -23,6 +31,9 @@ const Index: FC = () => {
},
};
const uiSchema: UISchema = {
permalink: {
'ui:widget': 'select',
},
robots: {
'ui:widget': 'textarea',
'ui:options': {
@ -37,6 +48,7 @@ const Index: FC = () => {
evt.stopPropagation();
const reqParams: Type.AdminSettingsSeo = {
permalink: Number(formData.permalink.value),
robots: formData.robots.value,
};
@ -60,6 +72,10 @@ const Index: FC = () => {
if (setting) {
const formMeta = { ...formData };
formMeta.robots.value = setting.robots;
formMeta.permalink.value = setting.permalink;
if (formMeta.permalink.value !== 1 && formMeta.permalink.value !== 2) {
formMeta.permalink.value = 1;
}
setFormData(formMeta);
}
});

View File

@ -6,12 +6,14 @@ import { getThemeSetting, putThemeSetting } from '@/services';
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
import { useToast } from '@/hooks';
import { handleFormError } from '@/utils';
import { themeSettingStore } from '@/stores';
const Index: FC = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'admin.themes',
});
const Toast = useToast();
const [themeSetting, setThemeSetting] = useState<Type.AdminSettingsTheme>();
const schema: JSONSchema = {
title: t('page_title'),
properties: {
@ -19,8 +21,8 @@ const Index: FC = () => {
type: 'string',
title: t('themes.label'),
description: t('themes.text'),
enum: ['default'],
enumNames: ['Default'],
enum: themeSetting?.theme_options?.map((_) => _.value),
enumNames: themeSetting?.theme_options?.map((_) => _.label),
default: 'default',
},
navbar_style: {
@ -51,8 +53,8 @@ const Index: FC = () => {
},
},
};
const [themeSetting, setThemeSetting] = useState<Type.AdminSettingsTheme>();
const [formData, setFormData] = useState(initFormData(schema));
const { update: updateThemeSetting } = themeSettingStore((_) => _);
const onSubmit = (evt) => {
evt.preventDefault();
evt.stopPropagation();
@ -72,6 +74,7 @@ const Index: FC = () => {
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
updateThemeSetting(reqParams);
})
.catch((err) => {
if (err.isError) {
@ -97,13 +100,13 @@ const Index: FC = () => {
}, []);
const handleOnChange = (cd) => {
console.log('cd: ', cd);
setFormData(cd);
const themeConfig = themeSetting?.theme_config[cd.themes.value];
if (themeConfig) {
themeConfig.navbar_style = cd.navbar_style.value;
themeConfig.primary_color = cd.primary_color.value;
setThemeSetting({
...themeSetting,
theme: themeSetting?.theme,
theme_config: themeSetting?.theme_config,
});

View File

@ -5,7 +5,7 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr';
import { toastStore, brandingStore, pageTagStore } from '@/stores';
import { Header, Footer, Toast, Customize } from '@/components';
import { Header, Footer, Toast, Customize, CustomizeTheme } from '@/components';
const Layout: FC = () => {
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
@ -31,6 +31,7 @@ const Layout: FC = () => {
{keywords && <meta name="keywords" content={keywords} />}
{description && <meta name="description" content={description} />}
</Helmet>
<CustomizeTheme />
<SWRConfig
value={{
revalidateOnFocus: false,

View File

@ -1,7 +1,7 @@
import urlcat from 'urlcat';
import Pattern from '@/common/pattern';
import { siteInfoStore } from '@/stores';
import { seoSettingStore } from '@/stores';
const tagLanding = (slugName: string) => {
if (!slugName) {
@ -21,8 +21,8 @@ const tagEdit = (tagId: string) => {
return urlcat('/tags/:tagId/edit', { tagId });
};
const questionLanding = (questionId: string, title: string = '') => {
const { siteInfo } = siteInfoStore.getState();
if (siteInfo.permalink === 1) {
const { seo } = seoSettingStore.getState();
if (seo.permalink === 1) {
title = title.toLowerCase();
title = title.trim().replace(/\s+/g, '-');
title = title.replace(Pattern.emoji, '');

View File

@ -22,19 +22,6 @@ export const updateGeneralSetting = (params: Type.AdminSettingsGeneral) => {
return request.put(apiUrl, params);
};
export const useThemeOptions = () => {
const apiUrl = `/answer/admin/api/theme/options`;
const { data, error } = useSWR<{ label: string; value: string }[]>(
[apiUrl],
request.instance.get,
);
return {
data,
isLoading: !data && !error,
error,
};
};
export const useInterfaceSetting = () => {
const apiUrl = `/answer/admin/api/siteinfo/interface`;
const { data, error } = useSWR<Type.AdminSettingsInterface, Error>(

View File

@ -1,4 +1,5 @@
import loginSettingStore from '@/stores/loginSetting';
import seoSettingStore from '@/stores/seoSetting';
import toastStore from './toast';
import loggedUserInfoStore from './userInfo';
@ -7,6 +8,7 @@ import interfaceStore from './interface';
import brandingStore from './branding';
import pageTagStore from './pageTags';
import customizeStore from './customize';
import themeSettingStore from './themeSetting';
export {
toastStore,
@ -17,4 +19,6 @@ export {
pageTagStore,
loginSettingStore,
customizeStore,
themeSettingStore,
seoSettingStore,
};

View File

@ -0,0 +1,27 @@
import create from 'zustand';
import { AdminSettingsSeo } from '@/common/interface';
interface IProps {
seo: AdminSettingsSeo;
update: (params: AdminSettingsSeo) => void;
}
const siteInfo = create<IProps>((set) => ({
seo: {
robots: '',
permalink: 1,
},
update: (params) =>
set((state) => {
const o = { ...state.seo, ...params };
if (o.permalink !== 1 && o.permalink !== 2) {
o.permalink = 1;
}
return {
seo: o,
};
}),
}));
export default siteInfo;

View File

@ -19,9 +19,6 @@ const siteInfo = create<SiteInfoType>((set) => ({
update: (params) =>
set((_) => {
const o = { ..._.siteInfo, ...params };
if (o.permalink !== 1 && o.permalink !== 2) {
o.permalink = 1;
}
return {
siteInfo: o,
};

View File

@ -0,0 +1,23 @@
import create from 'zustand';
import { AdminSettingsTheme } from '@/common/interface';
interface IType {
theme: AdminSettingsTheme['theme'];
theme_config: AdminSettingsTheme['theme_config'];
update: (params: AdminSettingsTheme) => void;
}
const store = create<IType>((set) => ({
theme: '',
theme_config: {},
update: (params) =>
set((state) => {
return {
...state,
...params,
};
}),
}));
export default store;

23
ui/src/utils/color.ts Normal file
View File

@ -0,0 +1,23 @@
import Color from 'color';
const WHITE = Color('#fff');
const BLACK = Color('#000');
export const mixColour = (baseColor, opColor, weight) => {
return Color(baseColor).mix(Color(opColor), weight);
};
export const tintColor = (color, weight) => {
return mixColour(WHITE, color, weight);
};
export const shadeColor = (color, weight) => {
return mixColour(BLACK, color, weight);
};
export const shiftColor = (color, weight) => {
if (weight > 0) {
return shadeColor(color, weight);
}
return tintColor(color, weight);
};

View File

@ -6,6 +6,8 @@ import {
brandingStore,
loginSettingStore,
customizeStore,
themeSettingStore,
seoSettingStore,
} from '@/stores';
import { RouteAlias } from '@/router/alias';
import Storage from '@/utils/storage';
@ -258,6 +260,8 @@ export const initAppSettingsStore = async () => {
brandingStore.getState().update(appSettings.branding);
loginSettingStore.getState().update(appSettings.login);
customizeStore.getState().update(appSettings.custom_css_html);
themeSettingStore.getState().update(appSettings.theme);
seoSettingStore.getState().update(appSettings.site_seo);
}
};

View File

@ -5,3 +5,4 @@ export { floppyNavigation } from './floppyNavigation';
export * as guard from './guard';
export * as localize from './localize';
export * from './common';
export * from './color';