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

View File

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

View File

@ -22,6 +22,7 @@ specifiers:
bootstrap-icons: 1.10.2 bootstrap-icons: 1.10.2
classnames: ^2.3.1 classnames: ^2.3.1
codemirror: 5.65.0 codemirror: 5.65.0
color: ^4.2.3
copy-to-clipboard: ^3.3.2 copy-to-clipboard: ^3.3.2
customize-cra: ^1.0.0 customize-cra: ^1.0.0
dayjs: ^1.11.5 dayjs: ^1.11.5
@ -74,6 +75,7 @@ dependencies:
bootstrap-icons: 1.10.2 bootstrap-icons: 1.10.2
classnames: 2.3.2 classnames: 2.3.2
codemirror: 5.65.0 codemirror: 5.65.0
color: 4.2.3
copy-to-clipboard: 3.3.2 copy-to-clipboard: 3.3.2
dayjs: 1.11.5 dayjs: 1.11.5
diff: 5.1.0 diff: 5.1.0
@ -3679,6 +3681,21 @@ packages:
/color-name/1.1.4: /color-name/1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 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: /colord/2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@ -6294,6 +6311,10 @@ packages:
/is-arrayish/0.2.1: /is-arrayish/0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
/is-arrayish/0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-bigint/1.0.4: /is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies: dependencies:
@ -9615,6 +9636,12 @@ packages:
/signal-exit/3.0.7: /signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 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: /sisteransi/1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}

View File

@ -279,12 +279,6 @@ export interface AdminSettingsGeneral {
description: string; description: string;
site_url: string; site_url: string;
contact_email: string; contact_email: string;
/**
* 0: not set
* 1with title
* 2: no title
*/
permalink: number;
} }
export interface HelmetBase { export interface HelmetBase {
@ -300,7 +294,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
export interface AdminSettingsInterface { export interface AdminSettingsInterface {
language: string; language: string;
theme: string;
time_zone?: string; time_zone?: string;
} }
@ -322,6 +315,8 @@ export interface SiteSettings {
interface: AdminSettingsInterface; interface: AdminSettingsInterface;
login: AdminSettingsLogin; login: AdminSettingsLogin;
custom_css_html: AdminSettingsCustom; custom_css_html: AdminSettingsCustom;
theme: AdminSettingsTheme;
site_seo: AdminSettingsSeo;
} }
export interface AdminSettingBranding { export interface AdminSettingBranding {
@ -346,6 +341,12 @@ export interface AdminSettingsWrite {
export interface AdminSettingsSeo { export interface AdminSettingsSeo {
robots: string; robots: string;
/**
* 0: not set
* 1with title
* 2: no title
*/
permalink: number;
} }
export type themeConfig = { export type themeConfig = {
@ -355,6 +356,7 @@ export type themeConfig = {
}; };
export interface AdminSettingsTheme { export interface AdminSettingsTheme {
theme: string; theme: string;
theme_options?: { label: string; value: string }[];
theme_config: Record<string, themeConfig>; 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} as={NavLink}
to="/users/notifications/inbox" to="/users/notifications/inbox"
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative"> 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" />
<Icon name="bell-fill" className="fs-4" />
</div>
{(redDot?.inbox || 0) > 0 && <div className="unread-dot bg-danger" />} {(redDot?.inbox || 0) > 0 && <div className="unread-dot bg-danger" />}
</Nav.Link> </Nav.Link>
@ -30,9 +28,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
as={Link} as={Link}
to="/users/notifications/achievement" to="/users/notifications/achievement"
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative"> 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" />
<Icon name="trophy-fill" className="fs-4" />
</div>
{(redDot?.achievement || 0) > 0 && ( {(redDot?.achievement || 0) > 0 && (
<div className="unread-dot bg-danger" /> <div className="unread-dot bg-danger" />
)} )}

View File

@ -1,32 +1,20 @@
@import 'bootstrap/scss/functions'; @import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables'; @import 'bootstrap/scss/variables';
#header { #header {
background: linear-gradient(180deg, #0033FF 0%, rgba(0, 51, 255, 0.95) 100%);
--bs-navbar-padding-y: 0.75rem; --bs-navbar-padding-y: 0.75rem;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15), $box-shadow-sm;
.logo { .logo {
max-height: 1.75rem; max-height: 1.75rem;
} }
.nav-link { .nav-link {
color: rgba(255, 255, 255, 0.7);
&.active {
font-weight: bold;
color: #fff;
}
&.icon-link { &.icon-link {
width: 36px; width: 36px;
height: 36px; height: 36px;
} }
} }
.placeholder-search { .placeholder-search {
background-color: rgba(255, 255, 255, .2); box-shadow: none;
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);
}
} }
.answer-navBar { .answer-navBar {
@ -45,6 +33,53 @@
.hr { .hr {
color: #fff; 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 { #header {
.logo { .logo {
max-width: 93px; max-width: 93px;
max-height: auto; max-height: initial;
} }
.nav-grow { .nav-grow {
flex-grow: 1!important; flex-grow: 1!important;
@ -73,7 +108,7 @@
#header { #header {
.logo { .logo {
max-width: 93px; max-width: 93px;
max-height: auto; max-height: initial;
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,14 @@ const Index: FC = () => {
const schema: JSONSchema = { const schema: JSONSchema = {
title: t('page_title'), title: t('page_title'),
properties: { 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: { robots: {
type: 'string', type: 'string',
title: t('robots.label'), title: t('robots.label'),
@ -23,6 +31,9 @@ const Index: FC = () => {
}, },
}; };
const uiSchema: UISchema = { const uiSchema: UISchema = {
permalink: {
'ui:widget': 'select',
},
robots: { robots: {
'ui:widget': 'textarea', 'ui:widget': 'textarea',
'ui:options': { 'ui:options': {
@ -37,6 +48,7 @@ const Index: FC = () => {
evt.stopPropagation(); evt.stopPropagation();
const reqParams: Type.AdminSettingsSeo = { const reqParams: Type.AdminSettingsSeo = {
permalink: Number(formData.permalink.value),
robots: formData.robots.value, robots: formData.robots.value,
}; };
@ -60,6 +72,10 @@ const Index: FC = () => {
if (setting) { if (setting) {
const formMeta = { ...formData }; const formMeta = { ...formData };
formMeta.robots.value = setting.robots; 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); setFormData(formMeta);
} }
}); });

View File

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

View File

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

View File

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

View File

@ -22,19 +22,6 @@ export const updateGeneralSetting = (params: Type.AdminSettingsGeneral) => {
return request.put(apiUrl, params); 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 = () => { export const useInterfaceSetting = () => {
const apiUrl = `/answer/admin/api/siteinfo/interface`; const apiUrl = `/answer/admin/api/siteinfo/interface`;
const { data, error } = useSWR<Type.AdminSettingsInterface, Error>( const { data, error } = useSWR<Type.AdminSettingsInterface, Error>(

View File

@ -1,4 +1,5 @@
import loginSettingStore from '@/stores/loginSetting'; import loginSettingStore from '@/stores/loginSetting';
import seoSettingStore from '@/stores/seoSetting';
import toastStore from './toast'; import toastStore from './toast';
import loggedUserInfoStore from './userInfo'; import loggedUserInfoStore from './userInfo';
@ -7,6 +8,7 @@ import interfaceStore from './interface';
import brandingStore from './branding'; import brandingStore from './branding';
import pageTagStore from './pageTags'; import pageTagStore from './pageTags';
import customizeStore from './customize'; import customizeStore from './customize';
import themeSettingStore from './themeSetting';
export { export {
toastStore, toastStore,
@ -17,4 +19,6 @@ export {
pageTagStore, pageTagStore,
loginSettingStore, loginSettingStore,
customizeStore, 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) => update: (params) =>
set((_) => { set((_) => {
const o = { ..._.siteInfo, ...params }; const o = { ..._.siteInfo, ...params };
if (o.permalink !== 1 && o.permalink !== 2) {
o.permalink = 1;
}
return { return {
siteInfo: o, 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, brandingStore,
loginSettingStore, loginSettingStore,
customizeStore, customizeStore,
themeSettingStore,
seoSettingStore,
} from '@/stores'; } from '@/stores';
import { RouteAlias } from '@/router/alias'; import { RouteAlias } from '@/router/alias';
import Storage from '@/utils/storage'; import Storage from '@/utils/storage';
@ -258,6 +260,8 @@ export const initAppSettingsStore = async () => {
brandingStore.getState().update(appSettings.branding); brandingStore.getState().update(appSettings.branding);
loginSettingStore.getState().update(appSettings.login); loginSettingStore.getState().update(appSettings.login);
customizeStore.getState().update(appSettings.custom_css_html); 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 guard from './guard';
export * as localize from './localize'; export * as localize from './localize';
export * from './common'; export * from './common';
export * from './color';