mirror of https://gitee.com/answerdev/answer.git
Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center
This commit is contained in:
commit
aa13657e8d
|
@ -709,6 +709,7 @@ ui:
|
|||
label: Confirm New Password
|
||||
settings:
|
||||
page_title: Settings
|
||||
goto_modify: Go to Modify
|
||||
nav:
|
||||
profile: Profile
|
||||
notification: Notifications
|
||||
|
|
|
@ -94,7 +94,7 @@ const Editor = ({
|
|||
return;
|
||||
}
|
||||
if (editor.getValue() !== value) {
|
||||
editor.setValue(value);
|
||||
editor.setValue(value || '');
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { NavLink, useNavigate } from 'react-router-dom';
|
|||
import type * as Type from '@/common/interface';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { userCenterStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
redDot: Type.NotificationStatus | undefined;
|
||||
|
@ -15,13 +16,14 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
const { t } = useTranslation();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const navigate = useNavigate();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const handleLinkClick = (evt) => {
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
const { href } = evt.currentTarget;
|
||||
const { pathname } = new URL(href);
|
||||
navigate(pathname);
|
||||
const href = evt.currentTarget.getAttribute('href');
|
||||
navigate(href);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
@ -90,6 +92,32 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{/* Dropdown for user center agent info */}
|
||||
{ucAgent?.enabled && ucAgent?.agent_info ? (
|
||||
<Dropdown align="end">
|
||||
<Dropdown.Toggle
|
||||
variant="success"
|
||||
id="dropdown-uca"
|
||||
as="span"
|
||||
className="no-toggle pointer nav-link p-0">
|
||||
<Icon name="grid-3x3-gap-fill" className="fs-4 ms-3" />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href={ucAgent.agent_info.url}>
|
||||
{ucAgent.agent_info.name}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
{ucAgent.agent_info.control_center?.map((ctrl) => {
|
||||
return (
|
||||
<Dropdown.Item key={ctrl.name} href={ctrl.url}>
|
||||
{ctrl.label}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { floppyNavigation, userCenter } from '@/utils';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
|
@ -29,6 +29,7 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -46,6 +47,9 @@ const Header: FC = () => {
|
|||
const brandingInfo = brandingStore((state) => state.branding);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const { data: redDot } = useQueryNotificationStatus();
|
||||
/**
|
||||
* Automatically append `tag` information when creating a question
|
||||
*/
|
||||
const tagMatch = useMatch('/tags/:slugName');
|
||||
let askUrl = '/questions/ask';
|
||||
if (tagMatch && tagMatch.params.slugName) {
|
||||
|
@ -70,16 +74,14 @@ const Header: FC = () => {
|
|||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
if (location.pathname === '/users/login') {
|
||||
if (location.pathname === RouteAlias.login) {
|
||||
evt.preventDefault();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
floppyNavigation.navigateToLogin();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -152,7 +154,7 @@ const Header: FC = () => {
|
|||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -160,7 +162,7 @@ const Header: FC = () => {
|
|||
variant={
|
||||
navbarStyle === 'theme-colored' ? 'light' : 'primary'
|
||||
}
|
||||
href="/users/register">
|
||||
href={userCenter.getSignUpUrl()}>
|
||||
{t('btns.signup')}
|
||||
</Button>
|
||||
)}
|
||||
|
@ -240,7 +242,7 @@ const Header: FC = () => {
|
|||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -248,7 +250,7 @@ const Header: FC = () => {
|
|||
variant={
|
||||
navbarStyle === 'theme-colored' ? 'light' : 'primary'
|
||||
}
|
||||
href="/users/register">
|
||||
href={userCenter.getSignUpUrl()}>
|
||||
{t('btns.signup')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -348,7 +348,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
useImperativeHandle(ref, () => ({
|
||||
validator,
|
||||
}));
|
||||
console.log('uiSchema: ', uiSchema);
|
||||
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{keys.map((key) => {
|
||||
|
|
|
@ -11,8 +11,11 @@ const Index = () => {
|
|||
const loginRedirect = () => {
|
||||
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
floppyNavigation.navigate(redirect, () => {
|
||||
navigate(redirect, { replace: true });
|
||||
floppyNavigation.navigate(redirect, {
|
||||
handler: navigate,
|
||||
options: {
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
useToast,
|
||||
} from '@/hooks';
|
||||
import { useQueryUsers, addUser, updateUserPassword } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { loggedUserInfoStore, userCenterStore } from '@/stores';
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
|
@ -49,6 +49,7 @@ const Users: FC = () => {
|
|||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const currentUser = loggedUserInfoStore((state) => state.user);
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const Toast = useToast();
|
||||
const {
|
||||
data,
|
||||
|
@ -149,12 +150,14 @@ const Users: FC = () => {
|
|||
sortKey="filter"
|
||||
i18nKeyPrefix="admin.users"
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => userModal.onShow()}>
|
||||
{t('add_user')}
|
||||
</Button>
|
||||
{!ucAgent?.enabled ? (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => userModal.onShow()}>
|
||||
{t('add_user')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<Form.Control
|
||||
|
@ -239,10 +242,12 @@ const Users: FC = () => {
|
|||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('password', user)}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
{!ucAgent?.enabled ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('password', user)}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
|
|
|
@ -302,7 +302,7 @@ const Ask = () => {
|
|||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
formData.content.value = revision.content?.content || '';
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import { FollowingTags, QuestionList, HotQuestions } from '@/components';
|
|||
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
|
||||
import { useQuestionList } from '@/services';
|
||||
import * as Type from '@/common/interface';
|
||||
import { userCenter } from '@/utils';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
@ -43,7 +44,7 @@ const Questions: FC = () => {
|
|||
/>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
{!loggedUser.access_token && (
|
||||
{!loggedUser.username && (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">
|
||||
|
@ -52,10 +53,12 @@ const Questions: FC = () => {
|
|||
})}
|
||||
</h5>
|
||||
<p className="card-text">{siteInfo.description}</p>
|
||||
<Link to="/users/login" className="btn btn-primary">
|
||||
<Link to={userCenter.getLoginUrl()} className="btn btn-primary">
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link to="/users/register" className="btn btn-link ms-2">
|
||||
<Link
|
||||
to={userCenter.getSignUpUrl()}
|
||||
className="btn btn-link ms-2">
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -94,8 +94,9 @@ const Index: React.FC = () => {
|
|||
const handleLoginRedirect = () => {
|
||||
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
floppyNavigation.navigate(redirect, () => {
|
||||
navigate(redirect, { replace: true });
|
||||
floppyNavigation.navigate(redirect, {
|
||||
handler: navigate,
|
||||
options: { replace: true },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -168,6 +169,9 @@ const Index: React.FC = () => {
|
|||
usePageTags({
|
||||
title: t('login', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
if (!guard.loginAgent().ok) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Avatar, Icon } from '@/components';
|
||||
import type { UserInfoRes } from '@/common/interface';
|
||||
import { getUcBranding, UcBrandingEntry } from '@/services';
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { base64ToSvg } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
data: UserInfoRes;
|
||||
|
@ -12,6 +17,22 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [ucBranding, setUcBranding] = useState<UcBrandingEntry[]>([]);
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled && data?.username) {
|
||||
getUcBranding(data.username).then((resp) => {
|
||||
if (resp.enabled && Array.isArray(resp.personal_branding)) {
|
||||
setUcBranding(resp.personal_branding);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, [data?.username]);
|
||||
if (!data?.username) {
|
||||
return null;
|
||||
}
|
||||
|
@ -65,27 +86,54 @@ const Index: FC<Props> = ({ data }) => {
|
|||
</div>
|
||||
|
||||
<div className="d-flex text-secondary">
|
||||
{data.location && (
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="geo-alt-fill" className="me-2" />
|
||||
<span>{data.location}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.website && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon name="house-door-fill" className="me-2" />
|
||||
<a
|
||||
className="link-secondary"
|
||||
href={
|
||||
data.website?.includes('http')
|
||||
? data.website
|
||||
: `http://${data.website}`
|
||||
}>
|
||||
{data?.website.replace(/(http|https):\/\//, '').split('/')?.[0]}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!ucAgent?.enabled ? (
|
||||
<>
|
||||
{data.location && (
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="geo-alt-fill" className="me-2" />
|
||||
<span>{data.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.website && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon name="house-door-fill" className="me-2" />
|
||||
<a
|
||||
className="link-secondary"
|
||||
href={
|
||||
data.website?.includes('http')
|
||||
? data.website
|
||||
: `http://${data.website}`
|
||||
}>
|
||||
{
|
||||
data?.website
|
||||
.replace(/(http|https):\/\//, '')
|
||||
.split('/')?.[0]
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{ucBranding.map((b, i) => {
|
||||
return (
|
||||
<div
|
||||
key={b.name}
|
||||
className={classnames('d-flex', 'align-items-center', {
|
||||
'ms-3': i > 0,
|
||||
})}>
|
||||
{b.icon ? (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base64ToSvg(b.icon),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<a className="link-secondary" href={b.url}>
|
||||
{b.label}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,14 +47,16 @@ const Personal: FC = () => {
|
|||
},
|
||||
tabName,
|
||||
);
|
||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||
|
||||
let pageTitle = '';
|
||||
if (userInfo?.username) {
|
||||
pageTitle = `${userInfo?.display_name} (${userInfo?.username})`;
|
||||
}
|
||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||
usePageTags({
|
||||
title: pageTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
useLegalTos,
|
||||
useLegalPrivacy,
|
||||
} from '@/services';
|
||||
import userStore from '@/stores/loggedUserInfoStore';
|
||||
import userStore from '@/stores/loggedUserInfo';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
|
@ -17,6 +18,9 @@ const Index: React.FC = () => {
|
|||
usePageTags({
|
||||
title: t('sign_up', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
if (!guard.singUpAgent().ok) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
|
|
|
@ -1,18 +1,46 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { getUcSettings } from '@/services';
|
||||
|
||||
import { ModifyEmail, ModifyPassword, MyLogins } from './components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.account',
|
||||
});
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [accountAgent, setAccountAgent] = useState('');
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled) {
|
||||
getUcSettings().then((resp) => {
|
||||
if (
|
||||
resp.account_setting_agent?.enabled &&
|
||||
resp.account_setting_agent?.redirect_url
|
||||
) {
|
||||
setAccountAgent(resp.account_setting_agent.redirect_url);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
{accountAgent ? (
|
||||
<a href={accountAgent}>{t('goto_modify', { keyPrefix: 'settings' })}</a>
|
||||
) : null}
|
||||
{!ucAgent?.enabled ? (
|
||||
<>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,9 +6,9 @@ import MD5 from 'md5';
|
|||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { UploadImg, Avatar, Icon } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { loggedUserInfoStore, userCenterStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
||||
import { modifyUserInfo, getLoggedUserInfo, getUcSettings } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
|
@ -17,8 +17,10 @@ const Index: React.FC = () => {
|
|||
});
|
||||
const toast = useToast();
|
||||
const { user, update } = loggedUserInfoStore();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [mailHash, setMailHash] = useState('');
|
||||
const [count] = useState(0);
|
||||
const [profileAgent, setProfileAgent] = useState('');
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
display_name: {
|
||||
|
@ -226,6 +228,7 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
const getProfile = () => {
|
||||
console.log('getProfile@Profile');
|
||||
getLoggedUserInfo().then((res) => {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.username.value = res.username;
|
||||
|
@ -243,228 +246,243 @@ const Index: React.FC = () => {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const refreshGravatar = () => {
|
||||
// setCount((pre) => pre + 1);
|
||||
// };
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled) {
|
||||
getUcSettings().then((resp) => {
|
||||
if (
|
||||
resp.profile_setting_agent?.enabled &&
|
||||
resp.profile_setting_agent?.redirect_url
|
||||
) {
|
||||
setProfileAgent(resp.profile_setting_agent.redirect_url);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getProfile();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
getProfile();
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="displayName" className="mb-3">
|
||||
<Form.Label>{t('display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.display_name.value}
|
||||
isInvalid={formData.display_name.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
display_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.display_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
{profileAgent ? (
|
||||
<a href={profileAgent}>{t('goto_modify', { keyPrefix: 'settings' })}</a>
|
||||
) : null}
|
||||
{!ucAgent?.enabled ? (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="displayName" className="mb-3">
|
||||
<Form.Label>{t('display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.display_name.value}
|
||||
isInvalid={formData.display_name.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
display_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.display_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="userName" className="mb-3">
|
||||
<Form.Label>{t('username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.username.value}
|
||||
isInvalid={formData.username.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="userName" className="mb-3">
|
||||
<Form.Label>{t('username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.username.value}
|
||||
isInvalid={formData.username.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||
<div className="mb-3">
|
||||
<Form.Select
|
||||
name="avatar.type"
|
||||
value={formData.avatar.type}
|
||||
onChange={handleAvatarChange}>
|
||||
<option value="gravatar" key="gravatar">
|
||||
{t('avatar.gravatar')}
|
||||
</option>
|
||||
<option value="default" key="default">
|
||||
{t('avatar.default')}
|
||||
</option>
|
||||
<option value="custom" key="custom">
|
||||
{t('avatar.custom')}
|
||||
</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
{formData.avatar.type === 'gravatar' && (
|
||||
<Stack>
|
||||
<Avatar
|
||||
size="160px"
|
||||
avatar={formData.avatar.gravatar}
|
||||
searchStr={`s=256&d=identicon${
|
||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||
}`}
|
||||
className="me-3 rounded"
|
||||
/>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.gravatar_text">
|
||||
You can change image on
|
||||
<a
|
||||
href="https://gravatar.com"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
gravatar.com
|
||||
</a>
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{formData.avatar.type === 'custom' && (
|
||||
<Stack>
|
||||
<Stack direction="horizontal" className="align-items-start">
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||
<div className="mb-3">
|
||||
<Form.Select
|
||||
name="avatar.type"
|
||||
value={formData.avatar.type}
|
||||
onChange={handleAvatarChange}>
|
||||
<option value="gravatar" key="gravatar">
|
||||
{t('avatar.gravatar')}
|
||||
</option>
|
||||
<option value="default" key="default">
|
||||
{t('avatar.default')}
|
||||
</option>
|
||||
<option value="custom" key="custom">
|
||||
{t('avatar.custom')}
|
||||
</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
{formData.avatar.type === 'gravatar' && (
|
||||
<Stack>
|
||||
<Avatar
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
avatar={formData.avatar.custom}
|
||||
className="me-2 bg-gray-300 "
|
||||
avatar={formData.avatar.gravatar}
|
||||
searchStr={`s=256&d=identicon${
|
||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||
}`}
|
||||
className="me-3 rounded"
|
||||
/>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg type="avatar" uploadCallback={avatarUpload}>
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={removeCustomAvatar}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.gravatar_text">
|
||||
You can change image on
|
||||
<a
|
||||
href="https://gravatar.com"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
gravatar.com
|
||||
</a>
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.text">
|
||||
You can upload your image.
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
{formData.avatar.type === 'default' && (
|
||||
<Avatar size="160px" avatar="" />
|
||||
)}
|
||||
</div>
|
||||
<Form.Control
|
||||
isInvalid={formData.avatar.isInvalid}
|
||||
className="d-none"
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.avatar.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
<Form.Group controlId="bio" className="mb-3">
|
||||
<Form.Label>
|
||||
{`${t('bio.label')} ${t('optional', {
|
||||
{formData.avatar.type === 'custom' && (
|
||||
<Stack>
|
||||
<Stack direction="horizontal" className="align-items-start">
|
||||
<Avatar
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
avatar={formData.avatar.custom}
|
||||
className="me-2 bg-gray-300 "
|
||||
/>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg type="avatar" uploadCallback={avatarUpload}>
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={removeCustomAvatar}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.text">
|
||||
You can upload your image.
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
{formData.avatar.type === 'default' && (
|
||||
<Avatar size="160px" avatar="" />
|
||||
)}
|
||||
</div>
|
||||
<Form.Control
|
||||
isInvalid={formData.avatar.isInvalid}
|
||||
className="d-none"
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.avatar.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="bio" className="mb-3">
|
||||
<Form.Label>
|
||||
{`${t('bio.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="font-monospace"
|
||||
required
|
||||
as="textarea"
|
||||
rows={5}
|
||||
value={formData.bio.value}
|
||||
isInvalid={formData.bio.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
bio: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.bio.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="website" className="mb-3">
|
||||
<Form.Label>{`${t('website.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="font-monospace"
|
||||
required
|
||||
as="textarea"
|
||||
rows={5}
|
||||
value={formData.bio.value}
|
||||
isInvalid={formData.bio.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
bio: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.bio.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="url"
|
||||
placeholder={t('website.placeholder')}
|
||||
value={formData.website.value}
|
||||
isInvalid={formData.website.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
website: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.website.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="website" className="mb-3">
|
||||
<Form.Label>{`${t('website.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="url"
|
||||
placeholder={t('website.placeholder')}
|
||||
value={formData.website.value}
|
||||
isInvalid={formData.website.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
website: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.website.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{`${t('location.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
placeholder={t('location.placeholder')}
|
||||
value={formData.location.value}
|
||||
isInvalid={formData.location.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
location: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.location.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{`${t('location.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
placeholder={t('location.placeholder')}
|
||||
value={formData.location.value}
|
||||
isInvalid={formData.location.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
location: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.location.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_name')}
|
||||
</Button>
|
||||
</Form>
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_name')}
|
||||
</Button>
|
||||
</Form>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Nav } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'settings.nav' });
|
||||
const settingMatch = useMatch('/users/settings/:setting');
|
||||
return (
|
||||
<Nav variant="pills" className="flex-column">
|
||||
<NavLink className="nav-link" to="/users/settings/profile">
|
||||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
isActive || !settingMatch ? 'nav-link active' : 'nav-link'
|
||||
}
|
||||
to="/users/settings/profile">
|
||||
{t('profile')}
|
||||
</NavLink>
|
||||
<NavLink className="nav-link" to="/users/settings/notify">
|
||||
|
|
|
@ -1,62 +1,17 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { FC, memo } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { getLoggedUserInfo } from '@/services';
|
||||
|
||||
import Nav from './components/Nav';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.profile',
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
display_name: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
avatar: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
bio: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
website: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
location: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
const getProfile = () => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
if (res) {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
formData.location.value = res.location;
|
||||
formData.website.value = res.website;
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getProfile();
|
||||
}, []);
|
||||
usePageTags({
|
||||
title: t('settings', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
@ -81,4 +36,4 @@ const Index: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default React.memo(Index);
|
||||
export default memo(Index);
|
||||
|
|
|
@ -21,8 +21,9 @@ const Index: FC<{
|
|||
const gr = onEnter();
|
||||
const redirectUrl = gr.redirect;
|
||||
if (redirectUrl) {
|
||||
floppyNavigation.navigate(redirectUrl, () => {
|
||||
navigate(redirectUrl, { replace: true });
|
||||
floppyNavigation.navigate(redirectUrl, {
|
||||
handler: navigate,
|
||||
options: { replace: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export const RouteAlias = {
|
||||
home: '/',
|
||||
login: '/users/login',
|
||||
register: '/users/register',
|
||||
signUp: '/users/register',
|
||||
activation: '/users/login?status=inactive',
|
||||
activationFailed: '/users/account-activation/failed',
|
||||
suspended: '/users/account-suspended',
|
||||
|
|
|
@ -158,8 +158,13 @@ const routes: RouteNode[] = [
|
|||
guard: () => {
|
||||
const notLogged = guard.notLogged();
|
||||
if (notLogged.ok) {
|
||||
const la = guard.loginAgent();
|
||||
if (!la.ok) {
|
||||
return la;
|
||||
}
|
||||
return notLogged;
|
||||
}
|
||||
|
||||
return guard.notActivated();
|
||||
},
|
||||
},
|
||||
|
@ -171,7 +176,14 @@ const routes: RouteNode[] = [
|
|||
if (!allowNew.ok) {
|
||||
return allowNew;
|
||||
}
|
||||
return guard.notLogged();
|
||||
const notLogged = guard.notLogged();
|
||||
if (notLogged.ok) {
|
||||
const sa = guard.singUpAgent();
|
||||
if (!sa.ok) {
|
||||
return sa;
|
||||
}
|
||||
}
|
||||
return notLogged;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -2,3 +2,4 @@ export * from './admin';
|
|||
export * from './common';
|
||||
export * from './client';
|
||||
export * from './install';
|
||||
export * from './user-center';
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import request from '@/utils/request';
|
||||
|
||||
export interface UcAgentControl {
|
||||
name: string;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
export interface UcAgent {
|
||||
enabled: boolean;
|
||||
agent_info: {
|
||||
name: string;
|
||||
icon: string;
|
||||
url: string;
|
||||
login_redirect_url: string;
|
||||
sign_up_redirect_url: string;
|
||||
control_center: UcAgentControl[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UcSettingAgent {
|
||||
enabled: boolean;
|
||||
redirect_url: string;
|
||||
}
|
||||
export interface UcSettings {
|
||||
profile_setting_agent: UcSettingAgent;
|
||||
account_setting_agent: UcSettingAgent;
|
||||
}
|
||||
|
||||
export interface UcBrandingEntry {
|
||||
icon: string;
|
||||
name: string;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
export interface UcBranding {
|
||||
enabled: boolean;
|
||||
personal_branding: UcBrandingEntry[];
|
||||
}
|
||||
|
||||
export const getUcAgent = () => {
|
||||
const apiUrl = `/answer/api/v1/user-center/agent`;
|
||||
return request.get<UcAgent>(apiUrl);
|
||||
};
|
||||
|
||||
export const getUcSettings = () => {
|
||||
const apiUrl = `/answer/api/v1/user-center/user/settings`;
|
||||
return request.get<UcSettings>(apiUrl);
|
||||
};
|
||||
|
||||
export const getUcBranding = (username: string) => {
|
||||
const apiUrl = `/answer/api/v1/user-center/personal/branding?username=${username}`;
|
||||
return request.get<UcBranding>(apiUrl);
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import loginSettingStore from '@/stores/loginSetting';
|
||||
import seoSettingStore from '@/stores/seoSetting';
|
||||
|
||||
import loginSettingStore from './loginSetting';
|
||||
import seoSettingStore from './seoSetting';
|
||||
import userCenterStore from './userCenter';
|
||||
import toastStore from './toast';
|
||||
import loggedUserInfoStore from './loggedUserInfoStore';
|
||||
import loggedUserInfoStore from './loggedUserInfo';
|
||||
import siteInfoStore from './siteInfo';
|
||||
import interfaceStore from './interface';
|
||||
import brandingStore from './branding';
|
||||
|
@ -25,4 +25,5 @@ export {
|
|||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
errorCode,
|
||||
userCenterStore,
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ const initUser: UserInfoRes = {
|
|||
role_id: 1,
|
||||
};
|
||||
|
||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
const loggedUserInfo = create<UserInfoStore>((set) => ({
|
||||
user: initUser,
|
||||
update: (params) => {
|
||||
if (!params.language) {
|
||||
|
@ -51,4 +51,4 @@ const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
export default loggedUserInfoStore;
|
||||
export default loggedUserInfo;
|
|
@ -0,0 +1,21 @@
|
|||
import create from 'zustand';
|
||||
|
||||
import type { UcAgent } from '@/services/user-center';
|
||||
|
||||
interface UserCenterStore {
|
||||
agent?: UcAgent;
|
||||
update: (uca: UcAgent) => void;
|
||||
}
|
||||
|
||||
const store = create<UserCenterStore>((set) => ({
|
||||
agent: undefined,
|
||||
update: (uca: UcAgent) => {
|
||||
if (uca) {
|
||||
set({
|
||||
agent: uca,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default store;
|
|
@ -1,6 +1,9 @@
|
|||
import type { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { getLoginUrl } from '@/utils/userCenter';
|
||||
|
||||
const differentCurrent = (target: string, base?: string) => {
|
||||
base ||= window.location.origin;
|
||||
|
@ -10,37 +13,70 @@ const differentCurrent = (target: string, base?: string) => {
|
|||
|
||||
const storageLoginRedirect = () => {
|
||||
const { pathname } = window.location;
|
||||
if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) {
|
||||
if (pathname !== RouteAlias.login && pathname !== RouteAlias.signUp) {
|
||||
const loc = window.location;
|
||||
const redirectUrl = loc.href.replace(loc.origin, '');
|
||||
Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determining whether an url is an external link
|
||||
*/
|
||||
const isExternalLink = (url = '') => {
|
||||
let ret = false;
|
||||
try {
|
||||
const urlObject = new URL(url, document.baseURI);
|
||||
if (urlObject && urlObject.origin !== window.location.origin) {
|
||||
ret = true;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (ex) {}
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* only navigate if not same as current url
|
||||
* @param pathname
|
||||
* @param callback
|
||||
*/
|
||||
const navigate = (pathname: string, callback: Function) => {
|
||||
if (pathname === RouteAlias.login) {
|
||||
storageLoginRedirect();
|
||||
type NavigateHandler = 'href' | 'replace' | NavigateFunction;
|
||||
interface NavigateConfig {
|
||||
handler: NavigateHandler;
|
||||
options?: any;
|
||||
}
|
||||
const navigate = (
|
||||
to: string | number,
|
||||
config: NavigateConfig = { handler: 'href' },
|
||||
) => {
|
||||
let { handler } = config;
|
||||
if (to && typeof to === 'string') {
|
||||
if (!differentCurrent(to)) {
|
||||
return;
|
||||
}
|
||||
if (to === RouteAlias.login || to === getLoginUrl()) {
|
||||
storageLoginRedirect();
|
||||
}
|
||||
if (isExternalLink(to)) {
|
||||
handler = 'href';
|
||||
}
|
||||
if (handler === 'href') {
|
||||
window.location.href = to;
|
||||
} else if (handler === 'replace') {
|
||||
window.location.replace(to);
|
||||
} else if (typeof handler === 'function') {
|
||||
handler(to, config.options);
|
||||
}
|
||||
}
|
||||
if (differentCurrent(pathname)) {
|
||||
callback();
|
||||
if (typeof to === 'number' && typeof handler === 'function') {
|
||||
handler(to);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* auto navigate to login page with redirect info
|
||||
*/
|
||||
const navigateToLogin = (callback?: Function) => {
|
||||
navigate(RouteAlias.login, () => {
|
||||
if (callback) {
|
||||
callback(RouteAlias.login);
|
||||
} else {
|
||||
window.location.replace(RouteAlias.login);
|
||||
}
|
||||
});
|
||||
const navigateToLogin = (config?: NavigateConfig) => {
|
||||
const loginUrl = getLoginUrl();
|
||||
navigate(loginUrl, config);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,6 +16,7 @@ import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
|||
import Storage from './storage';
|
||||
import { setupAppLanguage, setupAppTimeZone } from './localize';
|
||||
import { floppyNavigation } from './floppyNavigation';
|
||||
import { pullUcAgent, getLoginUrl, getSignUpUrl } from './userCenter';
|
||||
|
||||
type TLoginState = {
|
||||
isLogged: boolean;
|
||||
|
@ -79,20 +80,20 @@ export const isIgnoredPath = (ignoredPath: string | string[]) => {
|
|||
return !!matchingPath;
|
||||
};
|
||||
|
||||
let pullLock = false;
|
||||
let dedupeTimestamp = 0;
|
||||
let pluLock = false;
|
||||
let pluTimestamp = 0;
|
||||
export const pullLoggedUser = async (forceRePull = false) => {
|
||||
// only pull once if not force re-pull
|
||||
if (pullLock && !forceRePull) {
|
||||
if (pluLock && !forceRePull) {
|
||||
return;
|
||||
}
|
||||
// dedupe pull requests in this time span in 10 seconds
|
||||
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
||||
if (Date.now() - pluTimestamp < 1000 * 10) {
|
||||
return;
|
||||
}
|
||||
dedupeTimestamp = Date.now();
|
||||
pluTimestamp = Date.now();
|
||||
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
||||
dedupeTimestamp = 0;
|
||||
pluTimestamp = 0;
|
||||
if (!deriveLoginState().isLogged) {
|
||||
// load fallback userInfo from local storage
|
||||
const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY);
|
||||
|
@ -103,7 +104,7 @@ export const pullLoggedUser = async (forceRePull = false) => {
|
|||
console.error(ex);
|
||||
});
|
||||
if (loggedUserInfo) {
|
||||
pullLock = true;
|
||||
pluLock = true;
|
||||
loggedUserInfoStore.getState().update(loggedUserInfo);
|
||||
}
|
||||
};
|
||||
|
@ -198,6 +199,26 @@ export const allowNewRegistration = () => {
|
|||
return gr;
|
||||
};
|
||||
|
||||
export const loginAgent = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const loginUrl = getLoginUrl();
|
||||
if (loginUrl !== RouteAlias.login) {
|
||||
gr.ok = false;
|
||||
gr.redirect = loginUrl;
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
||||
export const singUpAgent = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const signUpUrl = getSignUpUrl();
|
||||
if (signUpUrl !== RouteAlias.signUp) {
|
||||
gr.ok = false;
|
||||
gr.redirect = signUpUrl;
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
||||
export const shouldLoginRequired = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const loginSetting = loginSettingStore.getState().login;
|
||||
|
@ -211,7 +232,7 @@ export const shouldLoginRequired = () => {
|
|||
if (
|
||||
isIgnoredPath([
|
||||
RouteAlias.login,
|
||||
RouteAlias.register,
|
||||
RouteAlias.signUp,
|
||||
'/users/account-recovery',
|
||||
'users/change-email',
|
||||
'users/password-reset',
|
||||
|
@ -246,12 +267,10 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
|||
return false;
|
||||
}
|
||||
if (us.isNotActivated) {
|
||||
floppyNavigation.navigate(RouteAlias.activation, () => {
|
||||
window.location.href = RouteAlias.activation;
|
||||
});
|
||||
floppyNavigation.navigate(RouteAlias.activation);
|
||||
} else if (us.isForbidden) {
|
||||
floppyNavigation.navigate(RouteAlias.suspended, () => {
|
||||
window.location.replace(RouteAlias.suspended);
|
||||
floppyNavigation.navigate(RouteAlias.suspended, {
|
||||
handler: 'replace',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -296,7 +315,11 @@ export const setupApp = async () => {
|
|||
* 2. must pre init app settings for app render
|
||||
*/
|
||||
// TODO: optimize `initAppSettingsStore` by server render
|
||||
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
||||
await Promise.allSettled([
|
||||
pullLoggedUser(),
|
||||
pullUcAgent(),
|
||||
initAppSettingsStore(),
|
||||
]);
|
||||
setupAppLanguage();
|
||||
setupAppTimeZone();
|
||||
appInitialized = true;
|
||||
|
|
|
@ -6,5 +6,6 @@ export { default as SaveDraft } from './saveDraft';
|
|||
|
||||
export * from './common';
|
||||
export * from './color';
|
||||
export * as userCenter from './userCenter';
|
||||
export * as localize from './localize';
|
||||
export * as guard from './guard';
|
||||
|
|
|
@ -24,7 +24,7 @@ export const loadLanguageOptions = async (forAdmin = false) => {
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
const { default: optConf } = await import('@i18n/i18n.yaml');
|
||||
optConf?.language_options.forEach((opt) => {
|
||||
if (!languageOptions.find((_) => opt.label === _.label)) {
|
||||
if (!languageOptions.find((_) => opt.value === _.value)) {
|
||||
languageOptions.push(opt);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -117,22 +117,20 @@ class Request {
|
|||
errorCode.getState().reset();
|
||||
if (data?.type === 'url_expired') {
|
||||
// url expired
|
||||
floppyNavigation.navigate(RouteAlias.activationFailed, () => {
|
||||
window.location.replace(RouteAlias.activationFailed);
|
||||
floppyNavigation.navigate(RouteAlias.activationFailed, {
|
||||
handler: 'replace',
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (data?.type === 'inactive') {
|
||||
// inactivated
|
||||
floppyNavigation.navigate(RouteAlias.activation, () => {
|
||||
window.location.href = RouteAlias.activation;
|
||||
});
|
||||
floppyNavigation.navigate(RouteAlias.activation);
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (data?.type === 'suspended') {
|
||||
floppyNavigation.navigate(RouteAlias.suspended, () => {
|
||||
window.location.replace(RouteAlias.suspended);
|
||||
floppyNavigation.navigate(RouteAlias.suspended, {
|
||||
handler: 'replace',
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { RouteAlias } from '@/router/alias';
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { getUcAgent, UcAgent } from '@/services/user-center';
|
||||
|
||||
export const pullUcAgent = async () => {
|
||||
const uca = await getUcAgent();
|
||||
userCenterStore.getState().update(uca);
|
||||
};
|
||||
|
||||
export const getLoginUrl = (uca?: UcAgent) => {
|
||||
let ret = RouteAlias.login;
|
||||
uca ||= userCenterStore.getState().agent;
|
||||
if (uca?.enabled && uca?.agent_info?.login_redirect_url) {
|
||||
ret = uca.agent_info.login_redirect_url;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const getSignUpUrl = (uca?: UcAgent) => {
|
||||
let ret = RouteAlias.signUp;
|
||||
uca ||= userCenterStore.getState().agent;
|
||||
if (uca?.enabled && uca?.agent_info?.sign_up_redirect_url) {
|
||||
ret = uca.agent_info.sign_up_redirect_url;
|
||||
}
|
||||
return ret;
|
||||
};
|
Loading…
Reference in New Issue