mirror of https://gitee.com/answerdev/answer.git
chore: admin/general,interface,seo, customize etc...
This commit is contained in:
parent
56bb48b80b
commit
30f7edd614
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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==}
|
||||
|
||||
|
|
|
@ -279,12 +279,6 @@ export interface AdminSettingsGeneral {
|
|||
description: string;
|
||||
site_url: string;
|
||||
contact_email: string;
|
||||
/**
|
||||
* 0: not set
|
||||
* 1:with 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
|
||||
* 1:with 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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
{(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>
|
||||
{(redDot?.achievement || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger" />
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,3 +5,4 @@ export { floppyNavigation } from './floppyNavigation';
|
|||
export * as guard from './guard';
|
||||
export * as localize from './localize';
|
||||
export * from './common';
|
||||
export * from './color';
|
||||
|
|
Loading…
Reference in New Issue