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:
parent
f76457c286
commit
bafd09474c
9 changed files with 261 additions and 29 deletions
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue