mirror of https://gitee.com/answerdev/answer.git
feat(local): set up app with language and time zone
This commit is contained in:
parent
549f816d3e
commit
7a8f5b67bc
|
@ -1,5 +1,6 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import './i18n/init';
|
||||
import { routes, createBrowserRouter } from '@/router';
|
||||
|
||||
function App() {
|
||||
|
|
|
@ -28,7 +28,8 @@ i18next
|
|||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
transSupportBasicHtmlNodes: true, // allow <br/> and simple html elements in translations
|
||||
transSupportBasicHtmlNodes: true,
|
||||
// allow <br/> and simple html elements in translations
|
||||
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
|
||||
},
|
||||
// backend: {
|
||||
|
|
|
@ -2,11 +2,10 @@ import React from 'react';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { Guard } from '@/utils';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
import App from './App';
|
||||
|
||||
import './i18n/init';
|
||||
import './index.scss';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
|
@ -14,10 +13,7 @@ const root = ReactDOM.createRoot(
|
|||
);
|
||||
|
||||
async function bootstrapApp() {
|
||||
/**
|
||||
* NOTICE: must pre init logged user info for router
|
||||
*/
|
||||
await Guard.pullLoggedUser();
|
||||
await guard.setupApp();
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
|
|
@ -12,7 +12,7 @@ import { interfaceStore } from '@/stores';
|
|||
import { UploadImg } from '@/components';
|
||||
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
|
||||
import {
|
||||
languages,
|
||||
getLanguageOptions,
|
||||
uploadAvatar,
|
||||
updateInterfaceSetting,
|
||||
useInterfaceSetting,
|
||||
|
@ -52,7 +52,7 @@ const Interface: FC = () => {
|
|||
},
|
||||
});
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await languages();
|
||||
const res: LangsType[] = await getLanguageOptions();
|
||||
setLangs(res);
|
||||
if (!formData.language.value) {
|
||||
// set default theme value
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import type { LangsType, FormValue, FormDataType } from '@/common/interface';
|
||||
import Progress from '../Progress';
|
||||
import { languages } from '@/services';
|
||||
import { getLanguageOptions } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: FormValue;
|
||||
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
|||
const [langs, setLangs] = useState<LangsType[]>();
|
||||
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await languages();
|
||||
const res: LangsType[] = await getLanguageOptions();
|
||||
setLangs(res);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,42 +1,19 @@
|
|||
import { FC, useEffect, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FC, memo } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { siteInfoStore, interfaceStore, toastStore } from '@/stores';
|
||||
import { siteInfoStore, toastStore } from '@/stores';
|
||||
import { Header, AdminHeader, Footer, Toast } from '@/components';
|
||||
import { useSiteSettings } from '@/services';
|
||||
import Storage from '@/utils/storage';
|
||||
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
let isMounted = false;
|
||||
const Layout: FC = () => {
|
||||
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
|
||||
const { update: interfaceStoreUpdate } = interfaceStore();
|
||||
const { data: siteSettings } = useSiteSettings();
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const { siteInfo } = siteInfoStore.getState();
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (siteSettings) {
|
||||
siteStoreUpdate(siteSettings.general);
|
||||
interfaceStoreUpdate(siteSettings.interface);
|
||||
}
|
||||
}, [siteSettings]);
|
||||
if (!isMounted) {
|
||||
isMounted = true;
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||
if (lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
|
@ -47,6 +24,7 @@ const Layout: FC = () => {
|
|||
revalidateOnFocus: false,
|
||||
}}>
|
||||
<Header />
|
||||
{/* TODO: move admin header to Admin/Index */}
|
||||
<AdminHeader />
|
||||
<div className="position-relative page-wrap">
|
||||
<Outlet />
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
} from '@/common/interface';
|
||||
import { PageTitle, Unactivate } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { getQueryString, Guard, floppyNavigation } from '@/utils';
|
||||
import { getQueryString, guard, floppyNavigation } from '@/utils';
|
||||
import { login, checkImgCode } from '@/services';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
@ -104,7 +104,7 @@ const Index: React.FC = () => {
|
|||
login(params)
|
||||
.then((res) => {
|
||||
updateUser(res);
|
||||
const userStat = Guard.deriveLoginState();
|
||||
const userStat = guard.deriveLoginState();
|
||||
if (userStat.isNotActivated) {
|
||||
// inactive
|
||||
setStep(2);
|
||||
|
@ -159,7 +159,7 @@ const Index: React.FC = () => {
|
|||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
||||
setStep(2);
|
||||
} else {
|
||||
Guard.tryNormalLogged();
|
||||
guard.tryNormalLogged();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -2,49 +2,54 @@ import React, { useEffect, useState, FormEvent } from 'react';
|
|||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import en from 'dayjs/locale/en';
|
||||
import zh from 'dayjs/locale/zh-cn';
|
||||
|
||||
import type { LangsType, FormDataType } from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { languages } from '@/services';
|
||||
import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
import { getLanguageOptions, updateUserInterface } from '@/services';
|
||||
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
import { localize } from '@/utils';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Index = () => {
|
||||
const { t, i18n } = useTranslation('translation', {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.interface',
|
||||
});
|
||||
const loggedUserInfo = loggedUserInfoStore.getState().user;
|
||||
const toast = useToast();
|
||||
const [langs, setLangs] = useState<LangsType[]>();
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
lang: {
|
||||
value: true,
|
||||
// FIXME: userinfo? or userInfo.language
|
||||
value: loggedUserInfo,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await languages();
|
||||
const res: LangsType[] = await getLanguageOptions();
|
||||
setLangs(res);
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value);
|
||||
dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh);
|
||||
i18n.changeLanguage(formData.lang.value);
|
||||
toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
const lang = formData.lang.value;
|
||||
updateUserInterface(lang).then(() => {
|
||||
loggedUserInfoStore.getState().update({
|
||||
...loggedUserInfo,
|
||||
language: lang,
|
||||
});
|
||||
localize.setupAppLanguage();
|
||||
toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getLangs();
|
||||
// TODO: get default lang by interface api
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||
if (lang) {
|
||||
setFormData({
|
||||
|
@ -74,7 +79,7 @@ const Index = () => {
|
|||
}}>
|
||||
{langs?.map((item) => {
|
||||
return (
|
||||
<option value={item.value} key={item.value}>
|
||||
<option value={item.value} key={item.label}>
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { RouteObject } from 'react-router-dom';
|
||||
|
||||
import { Guard } from '@/utils';
|
||||
import { guard } from '@/utils';
|
||||
import type { TGuardResult } from '@/utils/guard';
|
||||
|
||||
export interface RouteNode extends RouteObject {
|
||||
|
@ -21,7 +21,7 @@ const routes: RouteNode[] = [
|
|||
path: '/',
|
||||
page: 'pages/Layout',
|
||||
guard: async () => {
|
||||
return Guard.notForbidden();
|
||||
return guard.notForbidden();
|
||||
},
|
||||
children: [
|
||||
// question and answer
|
||||
|
@ -46,14 +46,14 @@ const routes: RouteNode[] = [
|
|||
path: 'questions/ask',
|
||||
page: 'pages/Questions/Ask',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'posts/:qid/edit',
|
||||
page: 'pages/Questions/Ask',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -81,7 +81,7 @@ const routes: RouteNode[] = [
|
|||
path: 'tags/:tagId/edit',
|
||||
page: 'pages/Tags/Edit',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
// users
|
||||
|
@ -97,7 +97,7 @@ const routes: RouteNode[] = [
|
|||
path: 'users/settings',
|
||||
page: 'pages/Users/Settings',
|
||||
guard: async () => {
|
||||
return Guard.logged();
|
||||
return guard.logged();
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
@ -130,25 +130,25 @@ const routes: RouteNode[] = [
|
|||
path: 'users/login',
|
||||
page: 'pages/Users/Login',
|
||||
guard: async () => {
|
||||
const notLogged = Guard.notLogged();
|
||||
const notLogged = guard.notLogged();
|
||||
if (notLogged.ok) {
|
||||
return notLogged;
|
||||
}
|
||||
return Guard.notActivated();
|
||||
return guard.notActivated();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'users/register',
|
||||
page: 'pages/Users/Register',
|
||||
guard: async () => {
|
||||
return Guard.notLogged();
|
||||
return guard.notLogged();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'users/account-recovery',
|
||||
page: 'pages/Users/AccountForgot',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -160,32 +160,32 @@ const routes: RouteNode[] = [
|
|||
path: 'users/password-reset',
|
||||
page: 'pages/Users/PasswordReset',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'users/account-activation',
|
||||
page: 'pages/Users/ActiveEmail',
|
||||
guard: async () => {
|
||||
const notActivated = Guard.notActivated();
|
||||
const notActivated = guard.notActivated();
|
||||
if (notActivated.ok) {
|
||||
return notActivated;
|
||||
}
|
||||
return Guard.notLogged();
|
||||
return guard.notLogged();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'users/account-activation/success',
|
||||
page: 'pages/Users/ActivationResult',
|
||||
guard: async () => {
|
||||
return Guard.activated();
|
||||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/account-activation/failed',
|
||||
page: 'pages/Users/ActivationResult',
|
||||
guard: async () => {
|
||||
return Guard.notActivated();
|
||||
return guard.notActivated();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ const routes: RouteNode[] = [
|
|||
path: '/users/account-suspended',
|
||||
page: 'pages/Users/Suspended',
|
||||
guard: async () => {
|
||||
return Guard.forbidden();
|
||||
return guard.forbidden();
|
||||
},
|
||||
},
|
||||
// for admin
|
||||
|
@ -205,8 +205,8 @@ const routes: RouteNode[] = [
|
|||
path: 'admin',
|
||||
page: 'pages/Admin',
|
||||
guard: async () => {
|
||||
await Guard.pullLoggedUser(true);
|
||||
return Guard.admin();
|
||||
await guard.pullLoggedUser(true);
|
||||
return guard.admin();
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './notification';
|
|||
export * from './question';
|
||||
export * from './search';
|
||||
export * from './tag';
|
||||
export * from './settings';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// import useSWR from 'swr';
|
||||
|
||||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export const loadLang = () => {
|
||||
return request.get('/answer/api/v1/language/config');
|
||||
};
|
||||
|
||||
export const getLanguageOptions = () => {
|
||||
return request.get<Type.LangsType[]>('/answer/api/v1/language/options');
|
||||
};
|
||||
|
||||
export const updateUserInterface = (lang: string) => {
|
||||
return request.put('/answer/api/v1/user/interface', {
|
||||
language: lang,
|
||||
});
|
||||
};
|
|
@ -163,14 +163,6 @@ export const questionDetail = (id: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const langConfig = () => {
|
||||
return request.get('/answer/api/v1/language/config');
|
||||
};
|
||||
|
||||
export const languages = () => {
|
||||
return request.get<Type.LangsType[]>('/answer/api/v1/language/options');
|
||||
};
|
||||
|
||||
export const getAnswers = (params: Type.AnswersReq) => {
|
||||
const apiUrl = `/answer/api/v1/answer/page?${qs.stringify(params)}`;
|
||||
return request.get<Type.ListResult<Type.AnswerItem>>(apiUrl);
|
||||
|
@ -253,16 +245,6 @@ export const changeEmailVerify = (params: { code: string }) => {
|
|||
return request.put('/answer/api/v1/user/email', params);
|
||||
};
|
||||
|
||||
export const useSiteSettings = () => {
|
||||
const apiUrl = `/answer/api/v1/siteinfo`;
|
||||
const { data, error } = useSWR<Type.SiteSettings, Error>(
|
||||
[apiUrl],
|
||||
request.instance.get,
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: !data && !error,
|
||||
error,
|
||||
};
|
||||
export const getAppSettings = () => {
|
||||
return request.get<Type.SiteSettings>('/answer/api/v1/siteinfo');
|
||||
};
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import create from 'zustand';
|
||||
|
||||
interface updateParams {
|
||||
logo: string;
|
||||
theme: string;
|
||||
language: string;
|
||||
}
|
||||
import { AdminSettingsInterface } from '@/common/interface';
|
||||
|
||||
interface InterfaceType {
|
||||
interface: updateParams;
|
||||
update: (params: updateParams) => void;
|
||||
interface: AdminSettingsInterface;
|
||||
update: (params: AdminSettingsInterface) => void;
|
||||
}
|
||||
|
||||
const interfaceSetting = create<InterfaceType>((set) => ({
|
||||
|
@ -16,6 +12,7 @@ const interfaceSetting = create<InterfaceType>((set) => ({
|
|||
logo: '',
|
||||
theme: '',
|
||||
language: '',
|
||||
time_zone: '',
|
||||
},
|
||||
update: (params) =>
|
||||
set(() => {
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import create from 'zustand';
|
||||
|
||||
interface updateParams {
|
||||
name: string;
|
||||
description: string;
|
||||
short_description: string;
|
||||
}
|
||||
import { AdminSettingsGeneral } from '@/common/interface';
|
||||
|
||||
interface SiteInfoType {
|
||||
siteInfo: updateParams;
|
||||
update: (params: updateParams) => void;
|
||||
siteInfo: AdminSettingsGeneral;
|
||||
update: (params: AdminSettingsGeneral) => void;
|
||||
}
|
||||
|
||||
const siteInfo = create<SiteInfoType>((set) => ({
|
||||
|
|
|
@ -25,6 +25,7 @@ const initUser: UserInfoRes = {
|
|||
website: '',
|
||||
status: '',
|
||||
mail_status: 1,
|
||||
language: '',
|
||||
};
|
||||
|
||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { getLoggedUserInfo } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { getLoggedUserInfo, getAppSettings } from '@/services';
|
||||
import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
||||
import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize';
|
||||
|
||||
import { floppyNavigation } from './floppyNavigation';
|
||||
|
||||
|
@ -180,3 +181,23 @@ export const tryNormalLogged = (autoLogin: boolean = false) => {
|
|||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const initAppSettingsStore = async () => {
|
||||
const appSettings = await getAppSettings();
|
||||
if (appSettings) {
|
||||
siteInfoStore.getState().update(appSettings.general);
|
||||
interfaceStore.getState().update(appSettings.interface);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupApp = async () => {
|
||||
/**
|
||||
* WARN:
|
||||
* 1. must pre init logged user info for router guard
|
||||
* 2. must pre init app settings for app render
|
||||
*/
|
||||
// TODO: optimize `initAppSettingsStore` by server render
|
||||
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
||||
setupAppLanguage();
|
||||
setupAppTimeZone();
|
||||
};
|
||||
|
|
|
@ -2,5 +2,6 @@ export { default as request } from './request';
|
|||
export { default as Storage } from './storage';
|
||||
export { floppyNavigation } from './floppyNavigation';
|
||||
|
||||
export * as Guard from './guard';
|
||||
export * as guard from './guard';
|
||||
export * as localize from './localize';
|
||||
export * from './common';
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import dayjs from 'dayjs';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
||||
import { DEFAULT_LANG } from '@/common/constants';
|
||||
|
||||
const localDayjs = (langName) => {
|
||||
langName = langName.replace('_', '-').toLowerCase();
|
||||
dayjs.locale(langName);
|
||||
};
|
||||
|
||||
export const getCurrentLang = () => {
|
||||
const loggedUser = loggedUserInfoStore.getState().user;
|
||||
const adminInterface = interfaceStore.getState().interface;
|
||||
let currentLang = loggedUser.language;
|
||||
// `default` mean use language value from admin interface
|
||||
if (/default/i.test(currentLang) && adminInterface.language) {
|
||||
currentLang = adminInterface.language;
|
||||
}
|
||||
currentLang ||= DEFAULT_LANG;
|
||||
return currentLang;
|
||||
};
|
||||
|
||||
export const setupAppLanguage = () => {
|
||||
const lang = getCurrentLang();
|
||||
localDayjs(lang);
|
||||
i18next.changeLanguage(lang);
|
||||
};
|
||||
|
||||
export const setupAppTimeZone = () => {
|
||||
// FIXME
|
||||
};
|
Loading…
Reference in New Issue