mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(ui): app notification (#999)
* feat(ui): app notification app notification * feat(ui): remove session storage remove session storage
This commit is contained in:
parent
3c37739107
commit
f4e380f0b1
9 changed files with 91 additions and 51 deletions
|
@ -72,7 +72,6 @@ const translation = {
|
||||||
social_create_account: 'No account? You can create a new account and bind.',
|
social_create_account: 'No account? You can create a new account and bind.',
|
||||||
social_bind_account: 'Already have an account? Sign in to bind it with your social identity.',
|
social_bind_account: 'Already have an account? Sign in to bind it with your social identity.',
|
||||||
social_bind_with_existing: 'We find a related account, you can bind it directly.',
|
social_bind_with_existing: 'We find a related account, you can bind it directly.',
|
||||||
demo_message: 'Use the Admin username and password to sign in this demo.',
|
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
username_password_mismatch: 'Username and password do not match.',
|
username_password_mismatch: 'Username and password do not match.',
|
||||||
|
|
|
@ -72,7 +72,6 @@ const translation = {
|
||||||
social_create_account: 'No account? You can create a new account and bind.',
|
social_create_account: 'No account? You can create a new account and bind.',
|
||||||
social_bind_account: 'Already have an account? Sign in to bind it with your social identity.',
|
social_bind_account: 'Already have an account? Sign in to bind it with your social identity.',
|
||||||
social_bind_with_existing: 'We find a related account, you can bind it directly.',
|
social_bind_with_existing: 'We find a related account, you can bind it directly.',
|
||||||
demo_message: '请使用 admin 用户名和密码登录',
|
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
username_password_mismatch: '用户名和密码不匹配。',
|
username_password_mismatch: '用户名和密码不匹配。',
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
@use '@/scss/underscore' as _;
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
.overlay {
|
|
||||||
background: transparent;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
padding: _.unit(3) _.unit(4);
|
padding: _.unit(3) _.unit(4);
|
||||||
font: var(--font-body);
|
font: var(--font-body);
|
||||||
|
@ -15,16 +10,13 @@
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
margin: 0 auto _.unit(2);
|
margin: 0 auto _.unit(2);
|
||||||
box-shadow: var(--shadow-1);
|
box-shadow: var(--shadow-1);
|
||||||
|
@include _.flex_row;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
@include _.flex_row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--color-outline);
|
color: var(--color-outline);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -42,23 +34,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.mobile) {
|
|
||||||
.overlay {
|
|
||||||
top: _.unit(6);
|
|
||||||
left: _.unit(5);
|
|
||||||
right: _.unit(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
:global(body.desktop) {
|
:global(body.desktop) {
|
||||||
.overlay {
|
|
||||||
top: _.unit(-6);
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|
|
@ -1,37 +1,28 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ReactModal, { Props as ModalProps } from 'react-modal';
|
|
||||||
|
|
||||||
import InfoIcon from '@/assets/icons/info-icon.svg';
|
import InfoIcon from '@/assets/icons/info-icon.svg';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = ModalProps & {
|
type Props = {
|
||||||
|
className?: string;
|
||||||
message: string;
|
message: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notification = ({ className, message, onClose, overlayClassName, ...rest }: Props) => {
|
const Notification = ({ className, message, onClose }: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<div className={classNames(styles.notification, className)}>
|
||||||
className={classNames(styles.notification, className)}
|
<InfoIcon className={styles.icon} />
|
||||||
overlayClassName={classNames(styles.overlay, overlayClassName)}
|
<div className={styles.message}>{message}</div>
|
||||||
ariaHideApp={false}
|
<a className={styles.link} onClick={onClose}>
|
||||||
parentSelector={() => document.querySelector('main') ?? document.body}
|
{t('action.got_it')}
|
||||||
onRequestClose={onClose}
|
</a>
|
||||||
{...rest}
|
</div>
|
||||||
>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<InfoIcon className={styles.icon} />
|
|
||||||
<div className={styles.message}>{message}</div>
|
|
||||||
<a className={styles.link} onClick={onClose}>
|
|
||||||
{t('action.got_it')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</ReactModal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
29
packages/ui/src/containers/AppNotification/index.module.scss
Normal file
29
packages/ui/src/containers/AppNotification/index.module.scss
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
|
||||||
|
.appNotification {
|
||||||
|
position: absolute;
|
||||||
|
top: _.unit(6);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:global(body.mobile) {
|
||||||
|
.appNotification {
|
||||||
|
top: _.unit(6);
|
||||||
|
left: _.unit(5);
|
||||||
|
right: _.unit(5);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:global(body.desktop) {
|
||||||
|
.appNotification {
|
||||||
|
top: _.unit(-6);
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
}
|
24
packages/ui/src/containers/AppNotification/index.test.tsx
Normal file
24
packages/ui/src/containers/AppNotification/index.test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { appNotificationStorageKey } from '@/utils/session-storage';
|
||||||
|
|
||||||
|
import AppNotification from '.';
|
||||||
|
|
||||||
|
describe('AppNotification', () => {
|
||||||
|
it('render properly', () => {
|
||||||
|
const message = 'This is a notification message';
|
||||||
|
sessionStorage.setItem(appNotificationStorageKey, message);
|
||||||
|
const { queryByText, getByText } = render(<AppNotification />);
|
||||||
|
|
||||||
|
expect(queryByText(message)).not.toBeNull();
|
||||||
|
|
||||||
|
const closeLink = getByText('action.got_it');
|
||||||
|
|
||||||
|
expect(closeLink).not.toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(closeLink);
|
||||||
|
|
||||||
|
expect(queryByText(message)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,21 +1,31 @@
|
||||||
|
import { Nullable } from '@silverhand/essentials';
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import Notification from '@/components/Notification';
|
import Notification from '@/components/Notification';
|
||||||
|
import { getAppNotificationInfo, clearAppNotificationInfo } from '@/utils/session-storage';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const AppNotification = () => {
|
const AppNotification = () => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
const [notification, setNotification] = useState<Nullable<string>>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setIsOpen(false);
|
setNotification(null);
|
||||||
|
clearAppNotificationInfo();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOpen(true);
|
const notification = getAppNotificationInfo();
|
||||||
|
setNotification(notification);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <Notification isOpen={isOpen} message={t('description.demo_message')} onClose={onClose} />;
|
if (!notification) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notification className={styles.appNotification} message={notification} onClose={onClose} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AppNotification;
|
export default AppNotification;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import classNames from 'classnames';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
import BrandingHeader from '@/components/BrandingHeader';
|
import BrandingHeader from '@/components/BrandingHeader';
|
||||||
|
import AppNotification from '@/containers/AppNotification';
|
||||||
import { PageContext } from '@/hooks/use-page-context';
|
import { PageContext } from '@/hooks/use-page-context';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
@ -35,6 +36,7 @@ const SignIn = () => {
|
||||||
socialConnectors={experienceSettings.socialConnectors}
|
socialConnectors={experienceSettings.socialConnectors}
|
||||||
/>
|
/>
|
||||||
<CreateAccountLink primarySignInMethod={experienceSettings.primarySignInMethod} />
|
<CreateAccountLink primarySignInMethod={experienceSettings.primarySignInMethod} />
|
||||||
|
<AppNotification />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
9
packages/ui/src/utils/session-storage.ts
Normal file
9
packages/ui/src/utils/session-storage.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const appNotificationStorageKey = 'logto:client:notification';
|
||||||
|
|
||||||
|
export const getAppNotificationInfo = () => {
|
||||||
|
return sessionStorage.getItem(appNotificationStorageKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearAppNotificationInfo = () => {
|
||||||
|
sessionStorage.removeItem(appNotificationStorageKey);
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue