0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(console): sie preview (#3277)

This commit is contained in:
Xiao Yijun 2023-03-06 10:18:34 +08:00 committed by GitHub
parent 1477751e30
commit c93b819b7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 198 deletions

View file

@ -0,0 +1,92 @@
@use '@/scss/underscore' as _;
.preview {
background: var(--color-surface-variant);
iframe {
border: none;
}
&.web {
.device {
width: 480px;
height: 380px;
position: relative;
background: var(--color-surface-1);
margin: 0 auto;
iframe {
width: 960px;
height: 760px;
transform: scale(0.5);
position: absolute;
top: -190px;
left: -240px;
background: var(--color-surface-1);
}
}
}
&.mobile {
padding: _.unit(10) 0;
height: 500px;
position: relative;
.deviceWrapper {
width: 390px;
height: 450px;
margin: 0 auto;
transform: scale(0.5);
transform-origin: top center;
.device {
border-radius: 26px;
overflow: hidden;
.topBar {
display: flex;
align-items: center;
padding: _.unit(3) _.unit(6);
.time {
flex: 1;
font: var(--font-label-2);
}
}
&.dark {
// Sync with iframe's UI color
background: #1a1c1d;
.topBar {
color: #fff;
}
}
&.light {
// Sync with iframe's UI color
background: #fff;
.topBar {
color: #000;
}
}
iframe {
width: 390px;
height: 808px;
}
}
@media screen and (min-height: 1100px) {
transform: unset;
height: 900px;
}
@media screen and (min-height: 900px) and (max-height: 1100px) {
transform: scale(0.75);
height: 675px;
}
}
}
}

View file

@ -0,0 +1,127 @@
import type { LanguageTag } from '@logto/language-kit';
import type { ConnectorMetadata, ConnectorResponse, SignInExperience } from '@logto/schemas';
import { ConnectorType, AppearanceMode } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import { format } from 'date-fns';
import { useContext, useRef, useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import PhoneInfo from '@/assets/images/phone-info.svg';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import type { RequestError } from '@/hooks/use-api';
import useUiLanguages from '@/hooks/use-ui-languages';
import * as styles from './index.module.scss';
import { PreviewPlatform } from './types';
type Props = {
platform: PreviewPlatform;
mode: Omit<AppearanceMode, AppearanceMode.SyncWithSystem>;
language: LanguageTag;
signInExperience?: SignInExperience;
};
const SignInExperiencePreview = ({ platform, mode, language, signInExperience }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { customPhrases } = useUiLanguages();
const { userEndpoint } = useContext(AppEndpointsContext);
const previewRef = useRef<HTMLIFrameElement>(null);
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('api/connectors');
const configForUiPage = useMemo(() => {
if (!allConnectors || !signInExperience) {
return;
}
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
Array<ConnectorMetadata & { id: string }>
>(
(previous, connectorTarget) => [
...previous,
...allConnectors.filter(({ target }) => target === connectorTarget),
],
[]
);
const hasEmailConnector = allConnectors.some(({ type }) => type === ConnectorType.Email);
const hasSmsConnector = allConnectors.some(({ type }) => type === ConnectorType.Sms);
return {
signInExperience: {
...signInExperience,
socialConnectors,
forgotPassword: {
email: hasEmailConnector,
sms: hasSmsConnector,
},
},
language,
mode,
platform: platform === PreviewPlatform.DesktopWeb ? 'web' : 'mobile',
isNative: platform === PreviewPlatform.Mobile,
};
}, [allConnectors, language, mode, platform, signInExperience]);
const postPreviewMessage = useCallback(() => {
if (!configForUiPage || !customPhrases) {
return;
}
previewRef.current?.contentWindow?.postMessage(
{ sender: 'ac_preview', config: configForUiPage },
userEndpoint?.origin ?? ''
);
}, [userEndpoint?.origin, configForUiPage, customPhrases]);
useEffect(() => {
postPreviewMessage();
const iframe = previewRef.current;
iframe?.addEventListener('load', postPreviewMessage);
return () => {
iframe?.removeEventListener('load', postPreviewMessage);
};
}, [postPreviewMessage]);
return (
<div
className={classNames(
styles.preview,
platform === PreviewPlatform.DesktopWeb ? styles.web : styles.mobile
)}
style={conditional(
platform === PreviewPlatform.DesktopWeb && {
// Set background color to match iframe's background color on both dark and light mode.
backgroundColor: mode === AppearanceMode.DarkMode ? '#000' : '#e5e1ec',
}
)}
>
<div className={styles.deviceWrapper}>
<div className={classNames(styles.device, styles[String(mode)])}>
{platform !== PreviewPlatform.DesktopWeb && (
<div className={styles.topBar}>
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
<PhoneInfo />
</div>
)}
<iframe
ref={previewRef}
// Allow all sandbox rules
sandbox={undefined}
src={new URL('/sign-in?preview=true', userEndpoint).toString()}
tabIndex={-1}
title={t('sign_in_exp.preview.title')}
/>
</div>
</div>
</div>
);
};
export default SignInExperiencePreview;

View file

@ -0,0 +1,5 @@
export enum PreviewPlatform {
DesktopWeb = 'desktopWeb',
Mobile = 'mobile',
MobileWeb = 'mobileWeb',
}

View file

@ -38,96 +38,4 @@
margin-top: _.unit(6);
border-bottom: unset;
}
.body {
flex: 1;
background: var(--color-surface-variant);
iframe {
border: none;
}
&.web {
.device {
width: 480px;
height: 380px;
position: relative;
background: var(--color-surface-1);
margin: 0 auto;
iframe {
width: 960px;
height: 760px;
transform: scale(0.5);
position: absolute;
top: -190px;
left: -240px;
background: var(--color-surface-1);
}
}
}
&.mobile {
padding: _.unit(10) 0;
height: 500px;
position: relative;
.deviceWrapper {
width: 390px;
height: 450px;
margin: 0 auto;
transform: scale(0.5);
transform-origin: top center;
.device {
border-radius: 26px;
overflow: hidden;
.topBar {
display: flex;
align-items: center;
padding: _.unit(3) _.unit(6);
.time {
flex: 1;
font: var(--font-label-2);
}
}
&.dark {
// Sync with iframe's UI color
background: #1a1c1d;
.topBar {
color: #fff;
}
}
&.light {
// Sync with iframe's UI color
background: #fff;
.topBar {
color: #000;
}
}
iframe {
width: 390px;
height: 808px;
}
}
@media screen and (min-height: 1100px) {
transform: unset;
height: 900px;
}
@media screen and (min-height: 900px) and (max-height: 1100px) {
transform: scale(0.75);
height: 675px;
}
}
}
}
}

View file

@ -1,21 +1,17 @@
import type { LanguageTag } from '@logto/language-kit';
import { languages as uiLanguageNameMapping } from '@logto/language-kit';
import type { ConnectorResponse, ConnectorMetadata, SignInExperience } from '@logto/schemas';
import { ConnectorType, AppearanceMode } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import type { SignInExperience } from '@logto/schemas';
import { AppearanceMode } from '@logto/schemas';
import classNames from 'classnames';
import { format } from 'date-fns';
import { useEffect, useMemo, useState, useRef, useCallback, useContext } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import PhoneInfo from '@/assets/images/phone-info.svg';
import LivePreviewButton from '@/components/LivePreviewButton';
import Select from '@/components/Select';
import SignInExperiencePreview from '@/components/SignInExperiencePreview';
import { PreviewPlatform } from '@/components/SignInExperiencePreview/types';
import TabNav, { TabNavItem } from '@/components/TabNav';
import ToggleThemeButton from '@/components/ToggleThemeButton';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import type { RequestError } from '@/hooks/use-api';
import useUiLanguages from '@/hooks/use-ui-languages';
import * as styles from './index.module.scss';
@ -38,11 +34,8 @@ const Preview = ({
const [mode, setMode] = useState<Omit<AppearanceMode, AppearanceMode.SyncWithSystem>>(
AppearanceMode.LightMode
);
const [platform, setPlatform] = useState<'desktopWeb' | 'mobile' | 'mobileWeb'>('desktopWeb');
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('api/connectors');
const previewRef = useRef<HTMLIFrameElement>(null);
const { customPhrases, languages } = useUiLanguages();
const { userEndpoint } = useContext(AppEndpointsContext);
const [platform, setPlatform] = useState<PreviewPlatform>(PreviewPlatform.DesktopWeb);
const { languages } = useUiLanguages();
useEffect(() => {
if (!signInExperience?.color.isDarkModeEnabled) {
@ -74,64 +67,6 @@ const Preview = ({
}
}, [language, availableLanguageOptions]);
const config = useMemo(() => {
if (!allConnectors || !signInExperience) {
return;
}
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
Array<ConnectorMetadata & { id: string }>
>(
(previous, connectorTarget) => [
...previous,
...allConnectors.filter(({ target }) => target === connectorTarget),
],
[]
);
const hasEmailConnector = allConnectors.some(({ type }) => type === ConnectorType.Email);
const hasSmsConnector = allConnectors.some(({ type }) => type === ConnectorType.Sms);
return {
signInExperience: {
...signInExperience,
socialConnectors,
forgotPassword: {
email: hasEmailConnector,
sms: hasSmsConnector,
},
},
language,
mode,
platform: platform === 'desktopWeb' ? 'web' : 'mobile',
isNative: platform === 'mobile',
};
}, [allConnectors, language, mode, platform, signInExperience]);
const postPreviewMessage = useCallback(() => {
if (!config || !customPhrases) {
return;
}
previewRef.current?.contentWindow?.postMessage(
{ sender: 'ac_preview', config },
userEndpoint?.origin ?? ''
);
}, [userEndpoint?.origin, config, customPhrases]);
useEffect(() => {
postPreviewMessage();
const iframe = previewRef.current;
iframe?.addEventListener('load', postPreviewMessage);
return () => {
iframe?.removeEventListener('load', postPreviewMessage);
};
}, [postPreviewMessage]);
return (
<div className={classNames(styles.preview, className)}>
<div className={styles.header}>
@ -158,58 +93,36 @@ const Preview = ({
</div>
<TabNav className={styles.nav}>
<TabNavItem
isActive={platform === 'desktopWeb'}
isActive={platform === PreviewPlatform.DesktopWeb}
onClick={() => {
setPlatform('desktopWeb');
setPlatform(PreviewPlatform.DesktopWeb);
}}
>
{t('sign_in_exp.preview.desktop_web')}
</TabNavItem>
<TabNavItem
isActive={platform === 'mobileWeb'}
isActive={platform === PreviewPlatform.MobileWeb}
onClick={() => {
setPlatform('mobileWeb');
setPlatform(PreviewPlatform.MobileWeb);
}}
>
{t('sign_in_exp.preview.mobile_web')}
</TabNavItem>
<TabNavItem
isActive={platform === 'mobile'}
isActive={platform === PreviewPlatform.Mobile}
onClick={() => {
setPlatform('mobile');
setPlatform(PreviewPlatform.Mobile);
}}
>
{t('sign_in_exp.preview.native')}
</TabNavItem>
</TabNav>
<div
className={classNames(styles.body, platform === 'desktopWeb' ? styles.web : styles.mobile)}
style={conditional(
platform === 'desktopWeb' && {
// Set background color to match iframe's background color on both dark and light mode.
backgroundColor: mode === AppearanceMode.DarkMode ? '#000' : '#e5e1ec',
}
)}
>
<div className={styles.deviceWrapper}>
<div className={classNames(styles.device, styles[String(mode)])}>
{platform !== 'desktopWeb' && (
<div className={styles.topBar}>
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
<PhoneInfo />
</div>
)}
<iframe
ref={previewRef}
// Allow all sandbox rules
sandbox={undefined}
src={new URL('/sign-in?preview=true', userEndpoint).toString()}
tabIndex={-1}
title={t('sign_in_exp.preview.title')}
/>
</div>
</div>
</div>
<SignInExperiencePreview
platform={platform}
mode={mode}
language={language}
signInExperience={signInExperience}
/>
</div>
);
};