mirror of
https://github.com/logto-io/logto.git
synced 2024-12-23 20:33:16 -05:00
refactor(console): refactor guide pages
This commit is contained in:
parent
1f55f779e6
commit
6bdf6cd928
7 changed files with 119 additions and 185 deletions
|
@ -1,12 +1,23 @@
|
|||
import React from 'react';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import i18next from 'i18next';
|
||||
import { MDXProps } from 'mdx/types';
|
||||
import React, { cloneElement, lazy, LazyExoticComponent, Suspense, useState } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import Guide from '@/pages/Guide';
|
||||
import Button from '@/components/Button';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import Close from '@/icons/Close';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { SupportedJavascriptLibraries } from '@/types/applications';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import LibrarySelector from '../LibrarySelector';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
|
@ -15,17 +26,101 @@ type Props = {
|
|||
onComplete: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
|
||||
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
|
||||
<Guide
|
||||
bannerComponent={<LibrarySelector />}
|
||||
title={appName}
|
||||
subtitle="applications.get_started.header_description"
|
||||
defaultSubtype={SupportedJavascriptLibraries.React}
|
||||
onClose={onClose}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react.mdx')),
|
||||
'react_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx')),
|
||||
};
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
const sampleUrl = `https://github.com/logto-io/js/tree/master/packages/${projectName}-sample`;
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => {
|
||||
const [subtype, setSubtype] = useState<string>(SupportedJavascriptLibraries.React);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(-1);
|
||||
const [invalidStepIndex, setInvalidStepIndex] = useState(-1);
|
||||
|
||||
const locale = i18next.language;
|
||||
const guideKey = `${subtype}_${locale}`.toLowerCase();
|
||||
const GuideComponent = Guides[guideKey] ?? Guides[subtype];
|
||||
|
||||
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
void onComplete(data);
|
||||
});
|
||||
|
||||
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={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
|
||||
<Button
|
||||
type="outline"
|
||||
title="admin_console.applications.guide.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(subtype);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
{cloneElement(<LibrarySelector />, {
|
||||
className: styles.banner,
|
||||
onChange: setSubtype,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
<MDXProvider
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return <CodeEditor isReadonly language={language} value={String(children)} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
{GuideComponent && (
|
||||
<GuideComponent
|
||||
activeStepIndex={activeStepIndex}
|
||||
invalidStepIndex={invalidStepIndex}
|
||||
onNext={(nextIndex: number) => {
|
||||
setActiveStepIndex(nextIndex);
|
||||
}}
|
||||
onError={(invalidIndex: number) => {
|
||||
setInvalidStepIndex(invalidIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</MDXProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideModal;
|
||||
|
|
|
@ -32,8 +32,8 @@ const LibrarySelector = ({
|
|||
<Card className={classNames(styles.card, className)}>
|
||||
<img src={highFive} alt="success" />
|
||||
<div>
|
||||
<div className={styles.title}>{t('applications.get_started.title')}</div>
|
||||
<div className={styles.subtitle}>{t('applications.get_started.subtitle')}</div>
|
||||
<div className={styles.title}>{t('applications.guide.title')}</div>
|
||||
<div className={styles.subtitle}>{t('applications.guide.subtitle')}</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
className={styles.radioGroup}
|
||||
|
@ -67,9 +67,7 @@ const LibrarySelector = ({
|
|||
() => (
|
||||
<div className={classNames(styles.card, styles.folded, className)}>
|
||||
<img src={tada} alt="Tada!" />
|
||||
<span>
|
||||
{t('applications.get_started.description_by_library', { library: libraryName })}
|
||||
</span>
|
||||
<span>{t('applications.guide.description_by_library', { library: libraryName })}</span>
|
||||
</div>
|
||||
),
|
||||
[className, libraryName, t]
|
||||
|
|
|
@ -29,11 +29,6 @@ type Props = {
|
|||
onComplete?: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const onClickFetchSampleProject = (name: string) => {
|
||||
const sampleUrl = `https://github.com/logto-io/js/tree/master/packages/connectors/${name}-sample`;
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
@ -88,19 +83,10 @@ const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{connectorName}</DangerousRaw>}
|
||||
subtitle="connectors.get_started.subtitle"
|
||||
subtitle="connectors.guide.subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
|
||||
{connectorName && (
|
||||
<Button
|
||||
type="outline"
|
||||
title="admin_console.get_started.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(connectorName);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<ReactMarkdown
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
import { AdminConsoleKey } from '@logto/phrases';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import i18next from 'i18next';
|
||||
import { MDXProps } from 'mdx/types';
|
||||
import React, {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
lazy,
|
||||
LazyExoticComponent,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
Suspense,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import Close from '@/icons/Close';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react.mdx')),
|
||||
'react_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx')),
|
||||
};
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle?: AdminConsoleKey;
|
||||
/** `subtype` can be an actual type of an application or connector.
|
||||
* e.g. React, Angular, Vue, etc. for application. Or Github, WeChat, etc. for connector.
|
||||
*/
|
||||
defaultSubtype?: string;
|
||||
bannerComponent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
onComplete?: (data: GuideForm) => Promise<void>;
|
||||
}>;
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
const sampleUrl = `https://github.com/logto-io/js/tree/master/packages/${projectName}-sample`;
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const Guide = ({
|
||||
title,
|
||||
subtitle,
|
||||
defaultSubtype = '',
|
||||
bannerComponent,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: Props) => {
|
||||
const [subtype, setSubtype] = useState(defaultSubtype);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(-1);
|
||||
const [invalidStepIndex, setInvalidStepIndex] = useState(-1);
|
||||
|
||||
const locale = i18next.language;
|
||||
const guideKey = `${subtype}_${locale}`.toLowerCase();
|
||||
const GuideComponent = Guides[guideKey] ?? Guides[subtype];
|
||||
|
||||
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
void onComplete?.(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle size="small" title={<DangerousRaw>{title}</DangerousRaw>} subtitle={subtitle} />
|
||||
<Spacer />
|
||||
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
|
||||
{subtype && (
|
||||
<Button
|
||||
type="outline"
|
||||
title="admin_console.get_started.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(subtype);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
{isValidElement(bannerComponent) &&
|
||||
cloneElement(bannerComponent, {
|
||||
className: styles.banner,
|
||||
onChange: setSubtype,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
<MDXProvider
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return <CodeEditor isReadonly language={language} value={String(children)} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
{GuideComponent && (
|
||||
<GuideComponent
|
||||
activeStepIndex={activeStepIndex}
|
||||
invalidStepIndex={invalidStepIndex}
|
||||
onNext={(nextIndex: number) => {
|
||||
setActiveStepIndex(nextIndex);
|
||||
}}
|
||||
onError={(invalidIndex: number) => {
|
||||
setInvalidStepIndex(invalidIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</MDXProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guide;
|
|
@ -153,7 +153,8 @@ const translation = {
|
|||
description: 'E.g.: Node.js, Express, ASP.NET, Java, PHP',
|
||||
},
|
||||
},
|
||||
get_started: {
|
||||
guide: {
|
||||
get_sample_file: 'Get a sample file',
|
||||
header_description:
|
||||
'Follow a step by step guide to integrate your application or get a sample configured with your account settings',
|
||||
title: 'Congratulations! The application has been created successfully.',
|
||||
|
@ -241,7 +242,7 @@ const translation = {
|
|||
sms: 'Setup SMS sender',
|
||||
social: 'Add social connector',
|
||||
},
|
||||
get_started: {
|
||||
guide: {
|
||||
subtitle:
|
||||
'A step by step guide to integrate your connector or get a sample configured with your account settings',
|
||||
},
|
||||
|
@ -267,9 +268,6 @@ const translation = {
|
|||
more_options: 'MORE OPTIONS',
|
||||
connector_deleted: 'The connector has been deleted.',
|
||||
},
|
||||
get_started: {
|
||||
get_sample_file: 'Get a sample file',
|
||||
},
|
||||
users: {
|
||||
title: 'User Management',
|
||||
subtitle:
|
||||
|
|
|
@ -153,7 +153,8 @@ const translation = {
|
|||
description: 'E.g.: Node.js, Express, ASP.NET, Java, PHP',
|
||||
},
|
||||
},
|
||||
get_started: {
|
||||
guide: {
|
||||
get_sample_file: '获取示例工程',
|
||||
header_description:
|
||||
'参考如下教程,将 Logto 集成到您的应用中。您也可以点击右侧链接,获取我们为您准备好的示范工程。',
|
||||
title: '恭喜!您的应用已成功创建。',
|
||||
|
@ -239,7 +240,7 @@ const translation = {
|
|||
sms: '设置短信服务商',
|
||||
social: '添加社会化登录',
|
||||
},
|
||||
get_started: {
|
||||
guide: {
|
||||
subtitle: '请参考下列分步指南,配置您的 connector,或点击按钮获取示例配置文件',
|
||||
},
|
||||
},
|
||||
|
@ -264,9 +265,6 @@ const translation = {
|
|||
more_options: '更多选项',
|
||||
connector_deleted: '成功删除连接器。',
|
||||
},
|
||||
get_started: {
|
||||
get_sample_file: '获取示例工程',
|
||||
},
|
||||
users: {
|
||||
title: '用户管理',
|
||||
subtitle: '管理已注册用户, 创建新用户,编辑用户资料。',
|
||||
|
|
Loading…
Reference in a new issue