0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(console): sign in exp guide (#755)

This commit is contained in:
Wang Sijie 2022-05-12 12:05:10 +08:00 committed by GitHub
parent f76457c286
commit bafd09474c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 261 additions and 29 deletions

View file

@ -8,6 +8,11 @@
border-radius: 8px;
align-items: center;
&.shadow {
border: 1px solid var(--color-border);
box-shadow: var(--shadow-1);
}
.icon {
width: 20px;
height: 20px;

View file

@ -5,6 +5,7 @@ import React, { ReactNode } from 'react';
import LinkButton from '@/components/LinkButton';
import Info from '@/icons/Info';
import Button from '../Button';
import * as styles from './index.module.scss';
type Props = {
@ -12,11 +13,20 @@ type Props = {
children?: ReactNode;
action?: I18nKey;
href?: string;
onClick?: () => void;
variant?: 'plain' | 'shadow';
};
const Alert = ({ children, action, href, severity = 'info' }: Props) => {
const Alert = ({
children,
action,
href,
onClick,
severity = 'info',
variant = 'plain',
}: Props) => {
return (
<div className={classNames(styles.alert, styles[severity])}>
<div className={classNames(styles.alert, styles[severity], styles[variant])}>
<div className={styles.icon}>
<Info />
</div>
@ -26,6 +36,11 @@ const Alert = ({ children, action, href, severity = 'info' }: Props) => {
<LinkButton title={action} to={href} />
</div>
)}
{action && onClick && (
<div className={styles.action}>
<Button title={action} type="plain" onClick={onClick} />
</div>
)}
</div>
);
};

View file

@ -0,0 +1,75 @@
@use '@/scss/underscore' as _;
.container {
background-color: var(--color-surface-1);
height: 100vh;
overflow-y: auto;
.header {
display: flex;
align-items: center;
background-color: var(--color-on-primary);
height: 64px;
padding: 0 _.unit(21) 0 _.unit(2);
button {
margin-left: _.unit(4);
}
.separator {
@include _.vertical-bar;
height: 20px;
margin: 0 _.unit(5) 0 _.unit(4);
}
}
.content {
overflow-y: auto;
padding: _.unit(6) _.unit(17);
// Space for footer
padding-bottom: 112px;
position: relative;
.reminder {
width: 550px;
margin: 0 auto _.unit(8);
}
}
.main {
display: flex;
.form {
flex: 1;
.card {
background: var(--color-layer-1);
padding: _.unit(2) _.unit(6) _.unit(6);
border-radius: 16px;
&:not(:last-child) {
margin-bottom: _.unit(6);
}
}
}
.preview {
width: 636px;
height: 800px;
background: var(--color-layer-1);
padding: _.unit(6);
border-radius: 16px;
margin-left: _.unit(8);
}
}
.footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--color-layer-1);
padding: _.unit(6) _.unit(20);
text-align: right;
}
}

View file

@ -0,0 +1,131 @@
import { SignInExperience } from '@logto/schemas';
import React, { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import useSWR from 'swr';
import Alert from '@/components/Alert';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import IconButton from '@/components/IconButton';
import Spacer from '@/components/Spacer';
import useApi from '@/hooks/use-api';
import useAdminConsoleConfigs from '@/hooks/use-configs';
import Close from '@/icons/Close';
import * as modalStyles from '@/scss/modal.module.scss';
import { SignInExperienceForm } from '../types';
import { signInExperienceParser } from '../utilities';
import BrandingForm from './BrandingForm';
import * as styles from './GuideModal.module.scss';
import LanguagesForm from './LanguagesForm';
import SignInMethodsForm from './SignInMethodsForm';
import TermsForm from './TermsForm';
type Props = {
isOpen: boolean;
onClose: () => void;
};
const GuideModal = ({ isOpen, onClose }: Props) => {
const { data } = useSWR<SignInExperience>('/api/sign-in-exp');
const { configs, updateConfigs } = useAdminConsoleConfigs();
const methods = useForm<SignInExperienceForm>();
const {
reset,
handleSubmit,
formState: { isSubmitting },
} = methods;
const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
useEffect(() => {
if (data) {
reset(signInExperienceParser.toLocalForm(data));
}
}, [data, reset]);
const onGotIt = async () => {
if (!configs) {
return;
}
await updateConfigs({ experienceNoticeConfirmed: true });
};
const onSubmit = handleSubmit(async (formData) => {
if (!data || isSubmitting || !configs) {
return;
}
await Promise.all([
api.patch('/api/sign-in-exp', {
json: signInExperienceParser.toRemoteModel(formData),
}),
updateConfigs({ experienceGuideDone: true }),
]);
location.reload();
});
return (
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
<div className={styles.container}>
<div className={styles.header}>
<IconButton size="large" onClick={onClose}>
<Close />
</IconButton>
<div className={styles.separator} />
<CardTitle size="small" title="sign_in_exp.title" subtitle="sign_in_exp.description" />
<Spacer />
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
</div>
<div className={styles.content}>
{configs && !configs.experienceNoticeConfirmed && (
<div className={styles.reminder}>
<Alert
action="admin_console.sign_in_exp.welcome.got_it"
variant="shadow"
onClick={onGotIt}
>
{t('sign_in_exp.welcome.apply_remind')}
</Alert>
</div>
)}
<FormProvider {...methods}>
<form onSubmit={onSubmit}>
<div className={styles.main}>
<div className={styles.form}>
<div className={styles.card}>
<BrandingForm />
</div>
<div className={styles.card}>
<TermsForm />
</div>
<div className={styles.card}>
<SignInMethodsForm />
</div>
<div className={styles.card}>
<LanguagesForm />
</div>
</div>
<div className={styles.preview}>TODO</div>
</div>
<div className={styles.footer}>
<Button
isLoading={isSubmitting}
type="primary"
htmlType="submit"
title="general.done"
/>
</div>
</form>
</FormProvider>
</div>
</div>
</Modal>
);
};
export default GuideModal;

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import WelcomeImage from '@/assets/images/sign-in-experience-welcome.svg';
@ -6,28 +6,36 @@ import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import GuideModal from './GuideModal';
import * as styles from './Welcome.module.scss';
type Props = {
onStart: () => void;
};
const Welcome = ({ onStart }: Props) => {
const Welcome = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isOpen, setIsOpen] = useState(false);
return (
<Card className={styles.welcome}>
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
<div className={styles.content}>
<img src={WelcomeImage} />
<div>{t('sign_in_exp.welcome.title')}</div>
<Button
title="admin_console.sign_in_exp.welcome.get_started"
type="primary"
onClick={onStart}
/>
</div>
</Card>
<>
<Card className={styles.welcome}>
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
<div className={styles.content}>
<img src={WelcomeImage} />
<div>{t('sign_in_exp.welcome.title')}</div>
<Button
title="admin_console.sign_in_exp.welcome.get_started"
type="primary"
onClick={() => {
setIsOpen(true);
}}
/>
</div>
</Card>
<GuideModal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
}}
/>
</>
);
};

View file

@ -33,7 +33,6 @@ const SignInExperience = () => {
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
const { configs, error: configError, updateConfigs } = useAdminConsoleConfigs();
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
const [showWelcome, setShowWelcome] = useState(!configs?.customizeSignInExperience);
const methods = useForm<SignInExperienceForm>();
const {
@ -86,14 +85,8 @@ const SignInExperience = () => {
return <div>{configError.body.message}</div>;
}
if (showWelcome) {
return (
<Welcome
onStart={() => {
setShowWelcome(false);
}}
/>
);
if (configs?.customizeSignInExperience) {
return <Welcome />;
}
return (

View file

@ -384,6 +384,8 @@ const translation = {
title:
'This is the first time you define your sign in experience. Follow the step by step guide and go through all of settings.',
get_started: 'Get started',
apply_remind: 'Sign in experience will apply to all applications under this account',
got_it: 'Got it',
},
branding: {
title: 'BRANDING',

View file

@ -380,6 +380,8 @@ const translation = {
title:
'This is the first time you define your sign in experience. Follow the step by step guide and go through all of settings.',
get_started: '开始',
apply_remind: 'Sign in experience will apply to all applications under this account',
got_it: 'Got it',
},
branding: {
title: 'BRANDING',

View file

@ -144,6 +144,7 @@ export const adminConsoleConfigGuard = z.object({
language: z.nativeEnum(Language),
appearanceMode: z.nativeEnum(AppearanceMode),
experienceNoticeConfirmed: z.boolean().optional(),
experienceGuideDone: z.boolean().optional(),
// Get started challenges
checkDemo: z.boolean(),
createApplication: z.boolean(),