mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): sign in experience preview (#783)
This commit is contained in:
parent
2b5254369b
commit
6ab54c968b
16 changed files with 208 additions and 54 deletions
|
@ -8,6 +8,7 @@
|
|||
a {
|
||||
color: var(--color-caption);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
25
packages/console/src/components/TabNav/TabNavItem.tsx
Normal file
25
packages/console/src/components/TabNav/TabNavItem.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import * as styles from './TabNavItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
href?: string;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const TabNavItem = ({ children, href, isActive, onClick }: Props) => {
|
||||
const location = useLocation();
|
||||
const selected = href ? location.pathname === href : isActive;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.link, selected && styles.selected)}>
|
||||
{href ? <Link to={href}>{children}</Link> : <a onClick={onClick}>{children}</a>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavItem;
|
|
@ -1,23 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import * as styles from './TabNavLink.module.scss';
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const TabNavLink = ({ children, href }: Props) => {
|
||||
const location = useLocation();
|
||||
const selected = location.pathname === href;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.link, selected && styles.selected)}>
|
||||
<Link to={href}>{children}</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavLink;
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export { default as TabNavLink } from './TabNavLink';
|
||||
export { default as TabNavItem } from './TabNavItem';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
|
|
@ -16,7 +16,7 @@ import Drawer from '@/components/Drawer';
|
|||
import FormField from '@/components/FormField';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Back from '@/icons/Back';
|
||||
|
@ -144,7 +144,7 @@ const ApiResourceDetails = () => {
|
|||
</Card>
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
<TabNav>
|
||||
<TabNavLink href={location.pathname}>{t('api_resource_details.settings')}</TabNavLink>
|
||||
<TabNavItem href={location.pathname}>{t('api_resource_details.settings')}</TabNavItem>
|
||||
</TabNav>
|
||||
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
|
||||
<div className={styles.fields}>
|
||||
|
|
|
@ -15,7 +15,7 @@ import CopyToClipboard from '@/components/CopyToClipboard';
|
|||
import Drawer from '@/components/Drawer';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Back from '@/icons/Back';
|
||||
import Delete from '@/icons/Delete';
|
||||
|
@ -166,12 +166,12 @@ const ApplicationDetails = () => {
|
|||
</Card>
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
<TabNav>
|
||||
<TabNavLink href={`/applications/${data.id}/settings`}>
|
||||
<TabNavItem href={`/applications/${data.id}/settings`}>
|
||||
{t('application_details.settings')}
|
||||
</TabNavLink>
|
||||
<TabNavLink href={`/applications/${data.id}/advanced-settings`}>
|
||||
</TabNavItem>
|
||||
<TabNavItem href={`/applications/${data.id}/advanced-settings`}>
|
||||
{t('application_details.advanced_settings')}
|
||||
</TabNavLink>
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
<FormProvider {...formMethods}>
|
||||
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
|
||||
|
|
|
@ -15,7 +15,7 @@ import ImagePlaceholder from '@/components/ImagePlaceholder';
|
|||
import LinkButton from '@/components/LinkButton';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import Status from '@/components/Status';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Back from '@/icons/Back';
|
||||
|
@ -182,9 +182,9 @@ const ConnectorDetails = () => {
|
|||
{data && (
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
<TabNav>
|
||||
<TabNavLink href={`/connectors/${connectorId ?? ''}`}>
|
||||
<TabNavItem href={`/connectors/${connectorId ?? ''}`}>
|
||||
{t('connector_details.tab_settings')}
|
||||
</TabNavLink>
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
<div className={styles.main}>
|
||||
<CodeEditor
|
||||
|
|
|
@ -8,7 +8,7 @@ import useSWR from 'swr';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import TableEmpty from '@/components/Table/TableEmpty';
|
||||
import TableError from '@/components/Table/TableError';
|
||||
import TableLoading from '@/components/Table/TableLoading';
|
||||
|
@ -67,8 +67,8 @@ const Connectors = () => {
|
|||
)}
|
||||
</div>
|
||||
<TabNav className={styles.tabs}>
|
||||
<TabNavLink href="/connectors">{t('connectors.tab_email_sms')}</TabNavLink>
|
||||
<TabNavLink href="/connectors/social">{t('connectors.tab_social')}</TabNavLink>
|
||||
<TabNavItem href="/connectors">{t('connectors.tab_email_sms')}</TabNavItem>
|
||||
<TabNavItem href="/connectors/social">{t('connectors.tab_social')}</TabNavItem>
|
||||
</TabNav>
|
||||
<div className={classNames(styles.table, tableStyles.scrollable)}>
|
||||
<table>
|
||||
|
|
|
@ -12,7 +12,7 @@ import Card from '@/components/Card';
|
|||
import CardTitle from '@/components/CardTitle';
|
||||
import FormField from '@/components/FormField';
|
||||
import Select from '@/components/Select';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import { themeStorageKey } from '@/consts';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
|
@ -57,7 +57,7 @@ const Settings = () => {
|
|||
<Card className={classNames(detailsStyles.container, styles.container)}>
|
||||
<CardTitle title="settings.title" subtitle="settings.description" />
|
||||
<TabNav>
|
||||
<TabNavLink href="/settings">{t('settings.tabs.general')}</TabNavLink>
|
||||
<TabNavItem href="/settings">{t('settings.tabs.general')}</TabNavItem>
|
||||
</TabNav>
|
||||
{!data && !error && <div>loading</div>}
|
||||
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.preview {
|
||||
width: 578px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin: _.unit(6) _.unit(6) 0;
|
||||
|
||||
.title {
|
||||
font: var(--font-subhead-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selects {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
width: 104px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: _.unit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 0 _.unit(6);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
import { AppearanceMode, SignInExperience } from '@logto/schemas';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import Select from '@/components/Select';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
|
||||
import * as styles from './Preview.module.scss';
|
||||
|
||||
type Props = {
|
||||
signInExperience: SignInExperience;
|
||||
};
|
||||
|
||||
const Preview = ({ signInExperience }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [language, setLanguage] = useState<Language>(Language.English);
|
||||
const [mode, setMode] = useState<AppearanceMode>(AppearanceMode.LightMode);
|
||||
const [platform, setPlatform] = useState<'web' | 'mobile'>('web');
|
||||
|
||||
// TODO: is a placeholder
|
||||
const config = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
...signInExperience,
|
||||
language,
|
||||
mode,
|
||||
platform,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={styles.preview}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{t('sign_in_exp.preview.title')}</div>
|
||||
<div className={styles.selects}>
|
||||
<Select
|
||||
value={language}
|
||||
options={[
|
||||
{ value: Language.English, title: t('sign_in_exp.preview.languages.english') },
|
||||
{ value: Language.Chinese, title: t('sign_in_exp.preview.languages.chinese') },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setLanguage(value as Language);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={mode}
|
||||
options={[
|
||||
{ value: AppearanceMode.LightMode, title: t('sign_in_exp.preview.light') },
|
||||
{ value: AppearanceMode.DarkMode, title: t('sign_in_exp.preview.dark') },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setMode(value as AppearanceMode);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TabNav className={styles.nav}>
|
||||
<TabNavItem
|
||||
isActive={platform === 'web'}
|
||||
onClick={() => {
|
||||
setPlatform('web');
|
||||
}}
|
||||
>
|
||||
{t('sign_in_exp.preview.web')}
|
||||
</TabNavItem>
|
||||
<TabNavItem
|
||||
isActive={platform === 'mobile'}
|
||||
onClick={() => {
|
||||
setPlatform('mobile');
|
||||
}}
|
||||
>
|
||||
{t('sign_in_exp.preview.mobile')}
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
<div className={styles.body}>
|
||||
<iframe src={`/sign-in?config=${config}`} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preview;
|
|
@ -22,8 +22,4 @@
|
|||
padding-bottom: _.unit(8);
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import useSWR from 'swr';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useAdminConsoleConfigs from '@/hooks/use-configs';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
|
@ -19,6 +19,7 @@ import * as modalStyles from '@/scss/modal.module.scss';
|
|||
|
||||
import BrandingForm from './components/BrandingForm';
|
||||
import LanguagesForm from './components/LanguagesForm';
|
||||
import Preview from './components/Preview';
|
||||
import SaveAlert from './components/SaveAlert';
|
||||
import SignInMethodsForm from './components/SignInMethodsForm';
|
||||
import TermsForm from './components/TermsForm';
|
||||
|
@ -39,9 +40,11 @@ const SignInExperience = () => {
|
|||
reset,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
const api = useApi();
|
||||
const formData = watch();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
|
@ -95,15 +98,15 @@ const SignInExperience = () => {
|
|||
<Card className={styles.card}>
|
||||
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
|
||||
<TabNav className={styles.tabs}>
|
||||
<TabNavLink href="/sign-in-experience/experience">
|
||||
<TabNavItem href="/sign-in-experience/experience">
|
||||
{t('sign_in_exp.tabs.experience')}
|
||||
</TabNavLink>
|
||||
<TabNavLink href="/sign-in-experience/methods">
|
||||
</TabNavItem>
|
||||
<TabNavItem href="/sign-in-experience/methods">
|
||||
{t('sign_in_exp.tabs.methods')}
|
||||
</TabNavLink>
|
||||
<TabNavLink href="/sign-in-experience/others">
|
||||
</TabNavItem>
|
||||
<TabNavItem href="/sign-in-experience/others">
|
||||
{t('sign_in_exp.tabs.others')}
|
||||
</TabNavLink>
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
{!data && !error && <div>loading</div>}
|
||||
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||
|
@ -135,7 +138,7 @@ const SignInExperience = () => {
|
|||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<Card className={styles.preview}>TODO</Card>
|
||||
{formData.id && <Preview signInExperience={signInExperienceParser.toRemoteModel(formData)} />}
|
||||
{data && (
|
||||
<ReactModal
|
||||
isOpen={Boolean(dataToCompare)}
|
||||
|
|
|
@ -17,7 +17,7 @@ import CopyToClipboard from '@/components/CopyToClipboard';
|
|||
import FormField from '@/components/FormField';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Back from '@/icons/Back';
|
||||
|
@ -179,8 +179,8 @@ const UserDetails = () => {
|
|||
</Card>
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
<TabNav>
|
||||
<TabNavLink href={`/users/${id}`}>{t('user_details.tab_settings')}</TabNavLink>
|
||||
<TabNavLink href={`/users/${id}/logs`}>{t('user_details.tab_logs')}</TabNavLink>
|
||||
<TabNavItem href={`/users/${id}`}>{t('user_details.tab_settings')}</TabNavItem>
|
||||
<TabNavItem href={`/users/${id}/logs`}>{t('user_details.tab_logs')}</TabNavItem>
|
||||
</TabNav>
|
||||
<form className={styles.form} onSubmit={onSubmit}>
|
||||
<div className={styles.fields}>
|
||||
|
|
|
@ -470,6 +470,17 @@ const translation = {
|
|||
before: 'Before',
|
||||
after: 'After',
|
||||
},
|
||||
preview: {
|
||||
title: 'Sign in preview',
|
||||
languages: {
|
||||
english: 'English',
|
||||
chinese: 'Chinese',
|
||||
},
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
mobile: 'Mobile',
|
||||
web: 'Web',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
|
|
|
@ -465,6 +465,17 @@ const translation = {
|
|||
before: '修改前',
|
||||
after: '修改后',
|
||||
},
|
||||
preview: {
|
||||
title: 'Sign in preview',
|
||||
languages: {
|
||||
english: 'English',
|
||||
chinese: 'Chinese',
|
||||
},
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
mobile: 'Mobile',
|
||||
web: 'Web',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
|
|
Loading…
Reference in a new issue