0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(ui): refactor ui loading (#1067)

* refactor(ui): refactor ui loading

refactor ui loading

* refactor(ui): refactor use-social-landing

refactor use-social-landing

* fix(ui): remove useless style

remove useless style

* fix(ui): fix typo

fix typo
This commit is contained in:
simeng-li 2022-06-09 10:27:53 +08:00 committed by GitHub
parent 0266c9aaf8
commit abf510eb8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 167 additions and 107 deletions

View file

@ -52,8 +52,6 @@ const translation = {
phone_number: 'phone number',
reminder: 'Reminder',
not_found: '404 Not Found',
loading: 'Loading...',
redirecting: 'Redirecting...',
agree_with_terms: 'I have read and agree to the ',
agree_with_terms_modal: 'Please read the {{terms}} and then agree the box first.',
terms_of_use: 'Terms of Use',

View file

@ -54,8 +54,6 @@ const translation = {
phone_number: '手机',
reminder: '提示',
not_found: '404 页面不存在',
loading: '读取中...',
redirecting: '页面跳转中...',
agree_with_terms: '我已阅读并同意 ',
agree_with_terms_modal: 'Please read the {{terms}} and then agree the box first.',
terms_of_use: '使用条款',

View file

@ -4,6 +4,7 @@ import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import * as styles from './App.module.scss';
import AppContent from './components/AppContent';
import LoadingLayerProvider from './containers/LoadingLayerProvider';
import usePageContext from './hooks/use-page-context';
import usePreview from './hooks/use-preview';
import initI18n from './i18n/init';
@ -52,25 +53,28 @@ const App = () => {
<AppContent>
<BrowserRouter>
<Routes>
{/* always keep route path with param as the last one */}
<Route path="/" element={<Navigate replace to="/sign-in" />} />
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-in/consent" element={<Consent />} />
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
<Route path="/register" element={<Register />} />
<Route path="/register/:method" element={<Register />} />
{/* social sign-in pages */}
<Route path="/sign-in/callback/:connector" element={<SocialSignInCallback />} />
<Route path="/callback/:connector" element={<Callback />} />
<Route path="/social/register/:connector" element={<SocialRegister />} />
<Route path="/social/landing/:connector" element={<SocialLanding />} />
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
<Route
path="/unknown-session"
element={<ErrorPage message="error.invalid_session" />}
/>
<Route element={<LoadingLayerProvider />}>
{/* always keep route path with param as the last one */}
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
<Route path="/register" element={<Register />} />
<Route path="/register/:method" element={<Register />} />
{/* social sign-in pages */}
<Route path="/sign-in/callback/:connector" element={<SocialSignInCallback />} />
<Route path="/callback/:connector" element={<Callback />} />
<Route path="/social/register/:connector" element={<SocialRegister />} />
<Route path="/social/landing/:connector" element={<SocialLanding />} />
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
</Route>
<Route path="*" element={<ErrorPage />} />
</Routes>
</BrowserRouter>

View file

@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.48" x="15" width="2" height="8" rx="1" fill="#191C1D"/>
<rect opacity="0.96" x="15" y="24" width="2" height="8" rx="1" fill="#191C1D"/>
<rect opacity="0.72" y="17" width="2" height="8" rx="1" transform="rotate(-90 0 17)" fill="#191C1D"/>
<rect opacity="0.24" x="24" y="17" width="2" height="8" rx="1" transform="rotate(-90 24 17)" fill="#191C1D"/>
<rect opacity="0.32" x="29.3564" y="7.13403" width="2" height="8" rx="1" transform="rotate(60 29.3564 7.13403)" fill="#191C1D"/>
<rect opacity="0.8" x="8.57227" y="19.134" width="2" height="8" rx="1" transform="rotate(60 8.57227 19.134)" fill="#191C1D"/>
<rect opacity="0.64" x="1.64355" y="8.86597" width="2" height="8" rx="1" transform="rotate(-60 1.64355 8.86597)" fill="#191C1D"/>
<rect opacity="0.16" x="22.4277" y="20.866" width="2" height="8" rx="1" transform="rotate(-60 22.4277 20.866)" fill="#191C1D"/>
<rect opacity="0.4" x="23.1338" y="1.64355" width="2" height="8" rx="1" transform="rotate(30 23.1338 1.64355)" fill="#191C1D"/>
<rect opacity="0.88" x="11.1338" y="22.4282" width="2" height="8" rx="1" transform="rotate(30 11.1338 22.4282)" fill="#191C1D"/>
<rect opacity="0.56" x="7.13379" y="2.64355" width="2" height="8" rx="1" transform="rotate(-30 7.13379 2.64355)" fill="#191C1D"/>
<rect opacity="0.08" x="19.1338" y="23.4282" width="2" height="8" rx="1" transform="rotate(-30 19.1338 23.4282)" fill="#191C1D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,14 +1,15 @@
<svg id="loading" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="15" width="2" height="8" rx="1" fill="white" fill-opacity="0.44"/>
<rect x="15" y="24" width="2" height="8" rx="1" fill="white"/>
<rect y="17" width="2" height="8" rx="1" transform="rotate(-90 0 17)" fill="white" fill-opacity="0.76"/>
<rect x="24" y="17" width="2" height="8" rx="1" transform="rotate(-90 24 17)" fill="white" fill-opacity="0.2"/>
<rect x="29.3564" y="7.13403" width="2" height="8" rx="1" transform="rotate(60 29.3564 7.13403)" fill="white" fill-opacity="0.28"/>
<rect x="8.57227" y="19.134" width="2" height="8" rx="1" transform="rotate(60 8.57227 19.134)" fill="white" fill-opacity="0.84"/>
<rect x="1.64355" y="8.86597" width="2" height="8" rx="1" transform="rotate(-60 1.64355 8.86597)" fill="white" fill-opacity="0.64"/>
<rect x="22.4277" y="20.866" width="2" height="8" rx="1" transform="rotate(-60 22.4277 20.866)" fill="white" fill-opacity="0.16"/>
<rect x="23.1338" y="1.64355" width="2" height="8" rx="1" transform="rotate(30 23.1338 1.64355)" fill="white" fill-opacity="0.36"/>
<rect x="11.1338" y="22.4282" width="2" height="8" rx="1" transform="rotate(30 11.1338 22.4282)" fill="white" fill-opacity="0.96"/>
<rect x="7.13379" y="2.64355" width="2" height="8" rx="1" transform="rotate(-30 7.13379 2.64355)" fill="white" fill-opacity="0.52"/>
<rect x="19.1338" y="23.4282" width="2" height="8" rx="1" transform="rotate(-30 19.1338 23.4282)" fill="white" fill-opacity="0.04"/>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.48" x="15" width="2" height="8" rx="1" fill="#F7F8F8"/>
<rect opacity="0.96" x="15" y="24" width="2" height="8" rx="1" fill="#F7F8F8"/>
<rect opacity="0.72" y="17" width="2" height="8" rx="1" transform="rotate(-90 0 17)" fill="#F7F8F8"/>
<rect opacity="0.24" x="24" y="17" width="2" height="8" rx="1" transform="rotate(-90 24 17)" fill="#F7F8F8"/>
<rect opacity="0.32" x="29.3564" y="7.13403" width="2" height="8" rx="1" transform="rotate(60 29.3564 7.13403)" fill="#F7F8F8"/>
<rect opacity="0.8" x="8.57227" y="19.134" width="2" height="8" rx="1" transform="rotate(60 8.57227 19.134)" fill="#F7F8F8"/>
<rect opacity="0.64" x="1.64355" y="8.86597" width="2" height="8" rx="1" transform="rotate(-60 1.64355 8.86597)" fill="#F7F8F8"/>
<rect opacity="0.16" x="22.4277" y="20.866" width="2" height="8" rx="1" transform="rotate(-60 22.4277 20.866)" fill="#F7F8F8"/>
<rect opacity="0.4" x="23.1338" y="1.64355" width="2" height="8" rx="1" transform="rotate(30 23.1338 1.64355)" fill="#F7F8F8"/>
<rect opacity="0.88" x="11.1338" y="22.4282" width="2" height="8" rx="1" transform="rotate(30 11.1338 22.4282)" fill="#F7F8F8"/>
<rect opacity="0.56" x="7.13379" y="2.64355" width="2" height="8" rx="1" transform="rotate(-30 7.13379 2.64355)" fill="#F7F8F8"/>
<rect opacity="0.08" x="19.1338" y="23.4282" width="2" height="8" rx="1" transform="rotate(-30 19.1338 23.4282)" fill="#F7F8F8"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,8 +1,6 @@
import { conditionalString } from '@silverhand/essentials';
import React, { ReactNode, useEffect, useCallback, useContext } from 'react';
import { useDebouncedLoader } from 'use-debounced-loader';
import LoadingLayer from '@/components/LoadingLayer';
import Toast from '@/components/Toast';
import useColorTheme from '@/hooks/use-color-theme';
import { PageContext } from '@/hooks/use-page-context';
@ -16,8 +14,7 @@ export type Props = {
const AppContent = ({ children }: Props) => {
const theme = useTheme();
const { toast, loading, platform, setToast, experienceSettings } = useContext(PageContext);
const debouncedLoading = useDebouncedLoader(loading);
const { toast, platform, setToast, experienceSettings } = useContext(PageContext);
// Prevent internal eventListener rebind
const hideToast = useCallback(() => {
@ -48,7 +45,6 @@ const AppContent = ({ children }: Props) => {
<main className={styles.content}>{children}</main>
{platform === 'web' && <div className={styles.placeHolder} />}
<Toast message={toast} isVisible={Boolean(toast)} callback={hideToast} />
{debouncedLoading && <LoadingLayer />}
</div>
);
};

View file

@ -0,0 +1,16 @@
import classNames from 'classnames';
import React from 'react';
import LoadingSvg from '@/assets/icons/loading-icon.svg';
import * as styles from './index.module.scss';
type Props = {
className?: string;
};
const LoadingIcon = ({ className }: Props) => (
<LoadingSvg className={classNames(styles.loadingIcon, className)} />
);
export default LoadingIcon;

View file

@ -0,0 +1,16 @@
import classNames from 'classnames';
import React from 'react';
import LoadingSvg from '@/assets/icons/loading-icon-light.svg';
import * as styles from './index.module.scss';
type Props = {
className?: string;
};
const LoadingIconLight = ({ className }: Props) => (
<LoadingSvg className={classNames(styles.loadingIcon, className)} />
);
export default LoadingIconLight;

View file

@ -20,7 +20,7 @@
}
.loadingIcon {
animation: rotating 1s linear infinite;
animation: rotating 1s steps(12, end) infinite;
}
@keyframes rotating {

View file

@ -1,13 +1,15 @@
import React from 'react';
import LoadingIcon from '@/assets/icons/loading-icon.svg';
import LoadingIcon from './LoadingIcon';
import * as styles from './index.module.scss';
export { default as LoadingIcon } from './LoadingIcon';
export { default as LoadingIconLight } from './LoadingIconLight';
const LoadingLayer = () => (
<div className={styles.overlay}>
<div className={styles.container}>
<LoadingIcon className={styles.loadingIcon} />
<LoadingIcon />
</div>
</div>
);

View file

@ -0,0 +1,20 @@
import React, { useContext } from 'react';
import { Outlet } from 'react-router-dom';
import { useDebouncedLoader } from 'use-debounced-loader';
import LoadingLayer from '@/components/LoadingLayer';
import { PageContext } from '@/hooks/use-page-context';
const LoadingLayerProvider = () => {
const { loading } = useContext(PageContext);
const debouncedLoading = useDebouncedLoader(loading);
return (
<>
<Outlet />
{debouncedLoading && <LoadingLayer />}
</>
);
};
export default LoadingLayerProvider;

View file

@ -1,22 +1,16 @@
@use '@/scss/underscore' as _;
.connector > img {
width: 96px;
height: 96px;
@include _.image-align-center;
}
.message {
margin-top: _.unit(2);
}
.container {
@include _.flex-column;
}
.connector {
margin-bottom: _.unit(4);
:global(body.desktop) {
.message {
margin-top: _.unit(1);
> img {
width: 96px;
height: 96px;
@include _.image-align-center;
}
}

View file

@ -1,6 +1,7 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import { LoadingIcon, LoadingIconLight } from '@/components/LoadingLayer';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';
@ -8,19 +9,20 @@ import * as styles from './index.module.scss';
type Props = {
className?: string;
connectorId: string;
message?: string;
isLoading?: boolean;
};
const SocialLanding = ({ className, connectorId, message }: Props) => {
const { experienceSettings } = useContext(PageContext);
const SocialLanding = ({ className, connectorId, isLoading = false }: Props) => {
const { experienceSettings, theme } = useContext(PageContext);
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);
const Loading = theme === 'light' ? LoadingIconLight : LoadingIcon;
return (
<div className={classNames(styles.container, className)}>
<div className={styles.connector}>
{connector?.logo ? <img src={connector.logo} /> : connectorId}
</div>
<div className={styles.message}>{message}</div>
{isLoading && <Loading />}
</div>
);
};

View file

@ -1,4 +1,4 @@
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@ -8,6 +8,7 @@ import { getCallbackLinkFromStorage } from '@/utils/social-connectors';
import { PageContext } from './use-page-context';
const useSocialCallbackHandler = () => {
const [loading, setLoading] = useState(true);
const { setToast } = useContext(PageContext);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const navigate = useNavigate();
@ -20,6 +21,7 @@ const useSocialCallbackHandler = () => {
// Connector auth error
if (error) {
setLoading(false);
setToast(`${error}${error_description ? `: ${error_description}` : ''}`);
return;
@ -27,6 +29,7 @@ const useSocialCallbackHandler = () => {
// Connector auth missing state
if (!state || !connectorId) {
setLoading(false);
setToast(t('error.invalid_connector_auth'));
return;
@ -55,7 +58,7 @@ const useSocialCallbackHandler = () => {
[navigate, setToast, t]
);
return socialCallbackHandler;
return { socialCallbackHandler, loading };
};
export default useSocialCallbackHandler;

View file

@ -1,4 +1,4 @@
import { useEffect, useContext } from 'react';
import { useContext, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SearchParameters } from '@/types';
@ -7,28 +7,35 @@ import { storeCallbackLink } from '@/utils/social-connectors';
import { PageContext } from './use-page-context';
const useSocialLandingHandler = (connectorId?: string) => {
const useSocialLandingHandler = () => {
const [loading, setLoading] = useState(true);
const { setToast } = useContext(PageContext);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const { search } = window.location;
useEffect(() => {
const redirectUri = getSearchParameters(search, SearchParameters.redirectTo);
const socialLandingHandler = useCallback(
(connectorId: string) => {
const redirectUri = getSearchParameters(search, SearchParameters.redirectTo);
if (!redirectUri || !connectorId) {
setToast(t('error.invalid_connector_request'));
if (!redirectUri) {
setLoading(false);
setToast(t('error.invalid_connector_request'));
return;
}
return;
}
const nativeCallbackLink = getSearchParameters(search, SearchParameters.nativeCallbackLink);
const nativeCallbackLink = getSearchParameters(search, SearchParameters.nativeCallbackLink);
if (nativeCallbackLink) {
storeCallbackLink(connectorId, nativeCallbackLink);
}
if (nativeCallbackLink) {
storeCallbackLink(connectorId, nativeCallbackLink);
}
window.location.replace(redirectUri);
}, [connectorId, search, setToast, t]);
window.location.replace(redirectUri);
},
[search, setToast, t]
);
return { loading, socialLandingHandler };
};
export default useSocialLandingHandler;

View file

@ -10,8 +10,3 @@
.connectorContainer {
flex: 1;
}
.button {
@include _.full-width;
margin-bottom: _.unit(4);
}

View file

@ -1,22 +1,19 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import Button from '@/components/Button';
import SocialLanding from '@/containers/SocialLanding';
import useSocialCallbackHandler from '@/hooks/use-social-callback-handler';
import * as styles from './index.module.scss';
type Props = {
type Parameters = {
connector: string;
};
const Callback = () => {
const { connector: connectorId } = useParams<Props>();
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const { connector: connectorId } = useParams<Parameters>();
const socialCallbackHandler = useSocialCallbackHandler();
const { socialCallbackHandler, loading } = useSocialCallbackHandler();
// SocialSignIn Callback Handler
useEffect(() => {
@ -35,16 +32,8 @@ const Callback = () => {
<SocialLanding
className={styles.connectorContainer}
connectorId={connectorId}
message={t('description.redirecting')}
isLoading={loading}
/>
<Button
className={styles.button}
onClick={() => {
socialCallbackHandler(connectorId);
}}
>
{t('action.continue')}
</Button>
</div>
);
};

View file

@ -8,10 +8,6 @@
width: 96px;
height: 96px;
@include _.image-align-center;
}
.content {
margin-top: _.unit(2);
@include _.text-hint;
margin-bottom: _.unit(4);
}
}

View file

@ -1,18 +1,22 @@
import React, { useEffect, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { consent } from '@/apis/consent';
import { LoadingIcon, LoadingIconLight } from '@/components/LoadingLayer';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';
const Consent = () => {
const { experienceSettings } = useContext(PageContext);
const logoUrl = experienceSettings?.branding.logoUrl;
const { result, run: asyncConsent } = useApi(consent);
const { experienceSettings, theme } = useContext(PageContext);
const { error, result, run: asyncConsent } = useApi(consent);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const logoUrl =
theme === 'light'
? experienceSettings?.branding.logoUrl
: experienceSettings?.branding.darkLogoUrl;
const Loading = theme === 'light' ? LoadingIconLight : LoadingIcon;
useEffect(() => {
void asyncConsent();
@ -27,7 +31,7 @@ const Consent = () => {
return (
<div className={styles.wrapper}>
<img src={logoUrl} />
<div className={styles.content}>{t('description.loading')}</div>
{!error && <Loading />}
</div>
);
};

View file

@ -1,5 +1,4 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import SocialLandingContainer from '@/containers/SocialLanding';
@ -13,9 +12,15 @@ type Parameters = {
const SocialLanding = () => {
const { connector: connectorId } = useParams<Parameters>();
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const { loading, socialLandingHandler } = useSocialLandingHandler();
useSocialLandingHandler(connectorId);
// SocialSignIn Callback Handler
useEffect(() => {
if (!connectorId) {
return;
}
socialLandingHandler(connectorId);
}, [connectorId, socialLandingHandler]);
if (!connectorId) {
return null;
@ -26,7 +31,7 @@ const SocialLanding = () => {
<SocialLandingContainer
className={styles.connectorContainer}
connectorId={connectorId}
message={t('description.redirecting')}
isLoading={loading}
/>
</div>
);