diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f55a2e3c..783ccd9e 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -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. diff --git a/ui/package.json b/ui/package.json index 12bcef86..f068b0e9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index cc92e65d..f4f44dc0 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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==} diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index e9ea4cce..124eecfe 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -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 { 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; } diff --git a/ui/src/components/CustomizeTheme/index.tsx b/ui/src/components/CustomizeTheme/index.tsx new file mode 100644 index 00000000..6bf13737 --- /dev/null +++ b/ui/src/components/CustomizeTheme/index.tsx @@ -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 ( + + {primaryColor && ( + + )} + + ); +}; + +export default Index; diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 17a3a683..9112aecc 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -20,9 +20,7 @@ const Index: FC = ({ 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"> -
- -
+ {(redDot?.inbox || 0) > 0 &&
} @@ -30,9 +28,7 @@ const Index: FC = ({ 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"> -
- -
+ {(redDot?.achievement || 0) > 0 && (
)} diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 5775e32a..c77c73b6 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -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; } } } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 257397bc..12900536 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -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 ( - + { ) : ( <> - {loginSetting.allow_new_registrations && ( - )} @@ -139,7 +152,7 @@ const Header: FC = () => {
handleInput(e.target.value)} @@ -163,7 +176,10 @@ const Header: FC = () => { + className={classnames('text-capitalize text-nowrap btn', { + 'btn-light': themeType !== 'theme-light', + 'btn-primary': themeType === 'theme-light', + })}> {t('btns.add_question')} @@ -178,12 +194,19 @@ const Header: FC = () => { <> {loginSetting.allow_new_registrations && ( - )} diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 8ce3b751..80b69eae 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -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 }; diff --git a/ui/src/index.scss b/ui/src/index.scss index ccd78c2c..662c1925 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -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 { diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 6e97b952..2b748fe2 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -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( 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]); diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index a401a7c5..f710b94f 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -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(); 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({ - 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) { diff --git a/ui/src/pages/Admin/Login/index.tsx b/ui/src/pages/Admin/Login/index.tsx index 94c3cb67..540b8bfc 100644 --- a/ui/src/pages/Admin/Login/index.tsx +++ b/ui/src/pages/Admin/Login/index.tsx @@ -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) { diff --git a/ui/src/pages/Admin/Seo/index.tsx b/ui/src/pages/Admin/Seo/index.tsx index 932d7d22..32744dc7 100644 --- a/ui/src/pages/Admin/Seo/index.tsx +++ b/ui/src/pages/Admin/Seo/index.tsx @@ -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); } }); diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx index cbec8607..55806339 100644 --- a/ui/src/pages/Admin/Themes/index.tsx +++ b/ui/src/pages/Admin/Themes/index.tsx @@ -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(); 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(); 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, }); diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 462b9beb..331aec3f 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -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 && } {description && } + { 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, ''); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index e458e472..82a97534 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -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( diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 03bad593..b8acdf73 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -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, }; diff --git a/ui/src/stores/seoSetting.ts b/ui/src/stores/seoSetting.ts new file mode 100644 index 00000000..800c480a --- /dev/null +++ b/ui/src/stores/seoSetting.ts @@ -0,0 +1,27 @@ +import create from 'zustand'; + +import { AdminSettingsSeo } from '@/common/interface'; + +interface IProps { + seo: AdminSettingsSeo; + update: (params: AdminSettingsSeo) => void; +} + +const siteInfo = create((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; diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index 98e7ecb8..978c10b4 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -19,9 +19,6 @@ const siteInfo = create((set) => ({ update: (params) => set((_) => { const o = { ..._.siteInfo, ...params }; - if (o.permalink !== 1 && o.permalink !== 2) { - o.permalink = 1; - } return { siteInfo: o, }; diff --git a/ui/src/stores/themeSetting.ts b/ui/src/stores/themeSetting.ts new file mode 100644 index 00000000..d9abbc2e --- /dev/null +++ b/ui/src/stores/themeSetting.ts @@ -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((set) => ({ + theme: '', + theme_config: {}, + update: (params) => + set((state) => { + return { + ...state, + ...params, + }; + }), +})); + +export default store; diff --git a/ui/src/utils/color.ts b/ui/src/utils/color.ts new file mode 100644 index 00000000..2864d191 --- /dev/null +++ b/ui/src/utils/color.ts @@ -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); +}; diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 2a99561b..140e28c0 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -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); } }; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index de9e8fef..180a1da0 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -5,3 +5,4 @@ export { floppyNavigation } from './floppyNavigation'; export * as guard from './guard'; export * as localize from './localize'; export * from './common'; +export * from './color';