mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(console): new app creation guide library (#4334)
* feat(console): consolidate all app guide metadata and use it with a hook * fix: disable "no-unused-modules" lint rule and use it in follow-up PRs * feat(console): new app creation guide library * fix: remove unused component * fix: eslint
This commit is contained in:
parent
1fe7d1aa33
commit
d0f91d5d37
41 changed files with 888 additions and 450 deletions
|
@ -26,7 +26,7 @@ type Props<
|
|||
subHeader?: ReactNode;
|
||||
table: TableProps<TFieldValues, TName>;
|
||||
/** @deprecated Need refactor. */
|
||||
widgets: ReactNode;
|
||||
widgets?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ function ConsoleContent() {
|
|||
<Route path="applications">
|
||||
<Route index element={<Applications />} />
|
||||
<Route path="create" element={<Applications />} />
|
||||
<Route path=":id/guide" element={<ApplicationDetails />} />
|
||||
<Route path=":id/guide/:guideId" element={<ApplicationDetails />} />
|
||||
<Route path=":id" element={<ApplicationDetails />} />
|
||||
</Route>
|
||||
<Route path="api-resources">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.group {
|
||||
> div:not(:last-child) {
|
||||
margin-bottom: _.unit(3);
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(3);
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ const mapToUriOriginFormatArrays = (value?: string[]) =>
|
|||
value?.filter(Boolean).map((uri) => decodeURIComponent(uri.replace(/\/*$/, '')));
|
||||
|
||||
function ApplicationDetails() {
|
||||
const { id } = useParams();
|
||||
const { id, guideId } = useParams();
|
||||
const { navigate, match } = useTenantPathname();
|
||||
const isGuideView = id && match(`/applications/${id}/guide`);
|
||||
const isGuideView = id && guideId && match(`/applications/${id}/guide/${guideId}`);
|
||||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data, error, mutate } = useSWR<ApplicationResponse, RequestError>(
|
||||
|
@ -243,6 +243,7 @@ function ApplicationDetails() {
|
|||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
|
||||
{isGuideView && (
|
||||
<GuideModal
|
||||
guideId={guideId}
|
||||
app={data}
|
||||
onClose={(id) => {
|
||||
navigate(`/applications/${id}`);
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: _.unit(5);
|
||||
|
||||
.title {
|
||||
font: var(--font-title-1);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: _.unit(1);
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: _.unit(6);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
max-width: 736px;
|
||||
gap: _.unit(4);
|
||||
|
||||
.option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 12px;
|
||||
padding: _.unit(3);
|
||||
|
||||
.createButton {
|
||||
margin-top: _.unit(2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import TypeDescription from '../TypeDescription';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: ApplicationType) => void;
|
||||
};
|
||||
|
||||
function ApplicationsPlaceholder({ onSelect }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
|
||||
const isMachineToMachineDisabled = !currentPlan?.quota.machineToMachineLimit;
|
||||
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<div className={styles.title}>{t('applications.placeholder_title')}</div>
|
||||
<div className={styles.description}>{t('applications.placeholder_description')}</div>
|
||||
<div className={styles.options}>
|
||||
{Object.values(ApplicationType).map((type) => (
|
||||
<div key={type} className={styles.option}>
|
||||
<TypeDescription
|
||||
size="small"
|
||||
type={type}
|
||||
title={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.subtitle`)}
|
||||
description={t(`${applicationTypeI18nKey[type]}.description`)}
|
||||
hasProTag={type === ApplicationType.MachineToMachine && isMachineToMachineDisabled}
|
||||
/>
|
||||
<Button
|
||||
className={styles.createButton}
|
||||
title="general.select"
|
||||
onClick={() => {
|
||||
onSelect(type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationsPlaceholder;
|
|
@ -1,7 +1,7 @@
|
|||
import type { Application } from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
@ -30,12 +30,11 @@ type FormData = {
|
|||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
defaultCreateType?: ApplicationType;
|
||||
onClose?: (createdApp?: Application) => void;
|
||||
};
|
||||
|
||||
function CreateForm({ isOpen, defaultCreateType, onClose }: Props) {
|
||||
function CreateForm({ defaultCreateType, onClose }: Props) {
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
|
||||
const isMachineToMachineDisabled = !currentPlan?.quota.machineToMachineLimit;
|
||||
|
@ -44,7 +43,6 @@ function CreateForm({ isOpen, defaultCreateType, onClose }: Props) {
|
|||
handleSubmit,
|
||||
control,
|
||||
register,
|
||||
resetField,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormData>();
|
||||
|
||||
|
@ -56,19 +54,9 @@ function CreateForm({ isOpen, defaultCreateType, onClose }: Props) {
|
|||
rules: { required: true },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultCreateType) {
|
||||
resetField('type', { defaultValue: defaultCreateType });
|
||||
}
|
||||
}, [defaultCreateType, resetField]);
|
||||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const api = useApi();
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async (data) => {
|
||||
if (isSubmitting) {
|
||||
|
@ -89,7 +77,7 @@ function CreateForm({ isOpen, defaultCreateType, onClose }: Props) {
|
|||
return (
|
||||
<Modal
|
||||
shouldCloseOnEsc
|
||||
isOpen={isOpen}
|
||||
isOpen
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
|
@ -104,37 +92,40 @@ function CreateForm({ isOpen, defaultCreateType, onClose }: Props) {
|
|||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
<FormField title="applications.select_application_type">
|
||||
<RadioGroup
|
||||
ref={ref}
|
||||
className={styles.radioGroup}
|
||||
name={name}
|
||||
value={value}
|
||||
type="card"
|
||||
onChange={onChange}
|
||||
>
|
||||
{Object.values(ApplicationType).map((type) => (
|
||||
<Radio
|
||||
key={type}
|
||||
value={type}
|
||||
hasCheckIconForCard={type !== ApplicationType.MachineToMachine}
|
||||
>
|
||||
<TypeDescription
|
||||
type={type}
|
||||
title={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.subtitle`)}
|
||||
description={t(`${applicationTypeI18nKey[type]}.description`)}
|
||||
hasProTag={
|
||||
type === ApplicationType.MachineToMachine && isMachineToMachineDisabled
|
||||
}
|
||||
/>
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{errors.type?.type === 'required' && (
|
||||
<div className={styles.error}>{t('applications.no_application_type_selected')}</div>
|
||||
)}
|
||||
</FormField>
|
||||
{defaultCreateType && <input hidden {...register('type')} value={defaultCreateType} />}
|
||||
{!defaultCreateType && (
|
||||
<FormField title="applications.select_application_type">
|
||||
<RadioGroup
|
||||
ref={ref}
|
||||
className={styles.radioGroup}
|
||||
name={name}
|
||||
value={value}
|
||||
type="card"
|
||||
onChange={onChange}
|
||||
>
|
||||
{Object.values(ApplicationType).map((type) => (
|
||||
<Radio
|
||||
key={type}
|
||||
value={type}
|
||||
hasCheckIconForCard={type !== ApplicationType.MachineToMachine}
|
||||
>
|
||||
<TypeDescription
|
||||
type={type}
|
||||
title={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.subtitle`)}
|
||||
description={t(`${applicationTypeI18nKey[type]}.description`)}
|
||||
hasProTag={
|
||||
type === ApplicationType.MachineToMachine && isMachineToMachineDisabled
|
||||
}
|
||||
/>
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{errors.type?.type === 'required' && (
|
||||
<div className={styles.error}>{t('applications.no_application_type_selected')}</div>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
<FormField isRequired title="applications.application_name">
|
||||
<TextInput
|
||||
{...register('name', { required: true })}
|
||||
|
|
|
@ -9,11 +9,12 @@ import GuideV2 from '../GuideV2';
|
|||
import Guide from '.';
|
||||
|
||||
type Props = {
|
||||
guideId: string;
|
||||
app?: Application;
|
||||
onClose: (id: string) => void;
|
||||
};
|
||||
|
||||
function GuideModal({ app, onClose }: Props) {
|
||||
function GuideModal({ guideId, app, onClose }: Props) {
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
@ -33,7 +34,7 @@ function GuideModal({ app, onClose }: Props) {
|
|||
{isProduction ? (
|
||||
<Guide app={app} onClose={closeModal} />
|
||||
) : (
|
||||
<GuideV2 app={app} onClose={closeModal} />
|
||||
<GuideV2 guideId={guideId} app={app} onClose={closeModal} />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ type Props = {
|
|||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
ios: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/ios.mdx')),
|
||||
android: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/android.mdx')),
|
||||
|
@ -77,12 +78,7 @@ function Guide({ app, isCompact, onClose }: Props) {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<GuideHeader
|
||||
appName={appName}
|
||||
selectedSdk={selectedSdk}
|
||||
isCompact={isCompact}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<GuideHeader isCompact={isCompact} onClose={onClose} />
|
||||
<div className={styles.content}>
|
||||
{cloneElement(<SdkSelector sdks={sdks} selectedSdk={selectedSdk} />, {
|
||||
className: styles.banner,
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(2);
|
||||
background-color: var(--color-layer-1);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: _.unit(3);
|
||||
min-width: 220px;
|
||||
max-width: 460px;
|
||||
justify-content: space-between;
|
||||
|
||||
&.hasBorder {
|
||||
border-color: var(--color-divider);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: _.unit(2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(1);
|
||||
}
|
||||
|
||||
.name {
|
||||
font: var(--font-label-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-3);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.logoSkeleton {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
@include _.shimmering-animation;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import classNames from 'classnames';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { type Guide, type GuideMetadata } from '@/assets/docs/guides/types';
|
||||
import Button from '@/ds-components/Button';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type SelectedGuide = {
|
||||
target: GuideMetadata['target'];
|
||||
id: Guide['id'];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: Guide;
|
||||
onClick: (data: SelectedGuide) => void;
|
||||
hasBorder?: boolean;
|
||||
};
|
||||
|
||||
function LogoSkeleton() {
|
||||
return <div className={styles.logoSkeleton} />;
|
||||
}
|
||||
|
||||
function GuideCard({ data, onClick, hasBorder }: Props) {
|
||||
const {
|
||||
id,
|
||||
Logo,
|
||||
metadata: { target, name, description },
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.card, hasBorder && styles.hasBorder)}>
|
||||
<div className={styles.header}>
|
||||
<Suspense fallback={<LogoSkeleton />}>
|
||||
<Logo className={styles.logo} />
|
||||
</Suspense>
|
||||
<div className={styles.infoWrapper}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
title="applications.guide.start_building"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onClick({ target, id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuideCard;
|
|
@ -3,9 +3,10 @@
|
|||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-layer-1);
|
||||
background-color: var(--color-base);
|
||||
height: 64px;
|
||||
padding: 0 _.unit(6);
|
||||
flex-shrink: 0;
|
||||
|
||||
.separator {
|
||||
@include _.vertical-bar;
|
||||
|
@ -31,7 +32,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.getSampleButton {
|
||||
margin: 0 _.unit(15) 0 _.unit(6);
|
||||
.requestSdkButton {
|
||||
margin-right: _.unit(2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +1,31 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Box from '@/assets/icons/box.svg';
|
||||
import Close from '@/assets/icons/close.svg';
|
||||
import GetSample from '@/assets/icons/get-sample.svg';
|
||||
import { githubIssuesLink } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import Button from '@/ds-components/Button';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import Spacer from '@/ds-components/Spacer';
|
||||
import Tooltip from '@/ds-components/Tip/Tooltip';
|
||||
import { SupportedSdk } from '@/types/applications';
|
||||
|
||||
import RequestGuide from './RequestGuide';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
selectedSdk: SupportedSdk;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const getSampleProjectUrl = (sdk: SupportedSdk) => {
|
||||
const githubUrlPrefix = 'https://github.com/logto-io';
|
||||
|
||||
switch (sdk) {
|
||||
case SupportedSdk.iOS: {
|
||||
return `${githubUrlPrefix}/swift/tree/master/Demos/SwiftUI%20Demo`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Android: {
|
||||
return `${githubUrlPrefix}/kotlin/tree/master/android-sample-kotlin`;
|
||||
}
|
||||
|
||||
case SupportedSdk.React: {
|
||||
return `${githubUrlPrefix}/js/tree/master/packages/react-sample`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Vue: {
|
||||
return `${githubUrlPrefix}/js/tree/master/packages/vue-sample`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Vanilla: {
|
||||
return `${githubUrlPrefix}/js/tree/master/packages/browser-sample`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Next: {
|
||||
return `${githubUrlPrefix}/js/tree/master/packages/next-sample`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Express: {
|
||||
return `${githubUrlPrefix}/js/tree/master/packages/express-sample`;
|
||||
}
|
||||
|
||||
case SupportedSdk.Go: {
|
||||
return `${githubUrlPrefix}/go/tree/master/gin-sample`;
|
||||
}
|
||||
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function GuideHeader({ appName, selectedSdk, isCompact = false, onClose }: Props) {
|
||||
function GuideHeader({ isCompact = false, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const onClickGetSample = () => {
|
||||
const sampleUrl = getSampleProjectUrl(selectedSdk);
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
const [isRequestGuideOpen, setIsRequestGuideOpen] = useState(false);
|
||||
const onRequestGuideClose = useCallback(() => {
|
||||
setIsRequestGuideOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
|
@ -75,19 +33,15 @@ function GuideHeader({ appName, selectedSdk, isCompact = false, onClose }: Props
|
|||
<>
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
title="applications.guide.modal_header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
anchorClassName={styles.githubToolTipAnchor}
|
||||
content={t('applications.guide.get_sample_file')}
|
||||
>
|
||||
<IconButton className={styles.githubIcon} size="large" onClick={onClickGetSample}>
|
||||
<GetSample />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
/>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
|
@ -101,19 +55,26 @@ function GuideHeader({ appName, selectedSdk, isCompact = false, onClose }: Props
|
|||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
title="applications.guide.modal_header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button type="text" size="small" title="general.skip" onClick={onClose} />
|
||||
<Button
|
||||
className={styles.getSampleButton}
|
||||
className={styles.requestSdkButton}
|
||||
type="outline"
|
||||
title="applications.guide.get_sample_file"
|
||||
onClick={onClickGetSample}
|
||||
icon={<Box />}
|
||||
title="applications.guide.cannot_find_guide"
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
setIsRequestGuideOpen(true);
|
||||
} else {
|
||||
window.open(githubIssuesLink, '_blank');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isCloud && <RequestGuide isOpen={isRequestGuideOpen} onClose={onRequestGuideClose} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-layer-1);
|
||||
height: 64px;
|
||||
padding: 0 _.unit(6);
|
||||
|
||||
.separator {
|
||||
@include _.vertical-bar;
|
||||
height: 20px;
|
||||
margin: 0 _.unit(5) 0 _.unit(4);
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.githubToolTipAnchor {
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
|
||||
.githubIcon {
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.requestSdkButton {
|
||||
margin-right: _.unit(15);
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
import Box from '@/assets/icons/box.svg';
|
||||
import Close from '@/assets/icons/close.svg';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import Button from '@/ds-components/Button';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import Spacer from '@/ds-components/Spacer';
|
||||
|
||||
import RequestGuide from './RequestGuide';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function GuideHeaderV2({ appName, isCompact = false, onClose }: Props) {
|
||||
const [isRequestGuideOpen, setIsRequestGuideOpen] = useState(false);
|
||||
const onRequestGuideClose = useCallback(() => {
|
||||
setIsRequestGuideOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{isCompact && (
|
||||
<>
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{!isCompact && (
|
||||
<>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button
|
||||
className={styles.requestSdkButton}
|
||||
type="outline"
|
||||
icon={<Box />}
|
||||
title="applications.guide.cannot_find_guide"
|
||||
onClick={() => {
|
||||
setIsRequestGuideOpen(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isCloud && <RequestGuide isOpen={isRequestGuideOpen} onClose={onRequestGuideClose} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuideHeaderV2;
|
|
@ -83,5 +83,4 @@ const useAppGuideMetadata = (): [
|
|||
return [getFilteredMetadata, getStructuredMetadata];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default useAppGuideMetadata;
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: _.unit(7);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(4);
|
||||
padding: _.unit(8) 0 _.unit(8) _.unit(11);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
label {
|
||||
font: var(--font-label-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
svg {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxGroup {
|
||||
gap: _.unit(4);
|
||||
}
|
||||
}
|
||||
|
||||
.groups {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: _.unit(8);
|
||||
overflow-y: auto;
|
||||
|
||||
> div {
|
||||
flex: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.guideGroup {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: _.unit(8) _.unit(8) 0 0;
|
||||
container-type: inline-size;
|
||||
|
||||
label {
|
||||
@include _.section-head-1;
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: _.unit(4) _.unit(3);
|
||||
}
|
||||
|
||||
@container (max-width: 680px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 681px) and (max-width: 1080px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 1081px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import { type Application } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type Guide } from '@/assets/docs/guides/types';
|
||||
import SearchIcon from '@/assets/icons/search.svg';
|
||||
import { CheckboxGroup } from '@/ds-components/Checkbox';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications';
|
||||
|
||||
import CreateForm from '../CreateForm';
|
||||
import GuideCard, { type SelectedGuide } from '../GuideCard';
|
||||
|
||||
import useAppGuideMetadata from './hook';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
hasFilter?: boolean;
|
||||
hasCardBorder?: boolean;
|
||||
};
|
||||
|
||||
type GuideGroupProps = {
|
||||
categoryName?: string;
|
||||
guides?: readonly Guide[];
|
||||
hasCardBorder?: boolean;
|
||||
onClickGuide: (data: SelectedGuide) => void;
|
||||
};
|
||||
|
||||
function GuideGroup({ categoryName, guides, hasCardBorder, onClickGuide }: GuideGroupProps) {
|
||||
if (!guides?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.guideGroup}>
|
||||
{categoryName && <label>{categoryName}</label>}
|
||||
<div className={styles.grid}>
|
||||
{guides.map((guide) => (
|
||||
<GuideCard key={guide.id} hasBorder={hasCardBorder} data={guide} onClick={onClickGuide} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideLibrary({ className, hasFilter, hasCardBorder }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.applications.guide' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
|
||||
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
|
||||
const [getFilteredMetadata, getStructuredMetadata] = useAppGuideMetadata();
|
||||
const [showCreateForm, setShowCreateForm] = useState<boolean>(false);
|
||||
|
||||
const structuredMetadata = useMemo(
|
||||
() => getStructuredMetadata({ categories: filterCategories }),
|
||||
[getStructuredMetadata, filterCategories]
|
||||
);
|
||||
|
||||
const filteredMetadata = useMemo(
|
||||
() => getFilteredMetadata({ keyword, categories: filterCategories }),
|
||||
[getFilteredMetadata, keyword, filterCategories]
|
||||
);
|
||||
|
||||
const onClickGuide = useCallback((data: SelectedGuide) => {
|
||||
setShowCreateForm(true);
|
||||
setSelectedGuide(data);
|
||||
}, []);
|
||||
|
||||
const onCloseCreateForm = useCallback(
|
||||
(newApp?: Application) => {
|
||||
if (newApp && selectedGuide) {
|
||||
navigate(`/applications/${newApp.id}/guide/${selectedGuide.id}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
setShowCreateForm(false);
|
||||
setSelectedGuide(undefined);
|
||||
},
|
||||
[navigate, selectedGuide]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{hasFilter && (
|
||||
<div className={styles.filters}>
|
||||
<label>{t('filter.title')}</label>
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon />}
|
||||
placeholder={t('filter.placeholder')}
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
options={allAppGuideCategories.map((category) => ({
|
||||
title: `applications.guide.categories.${category}`,
|
||||
value: category,
|
||||
}))}
|
||||
value={filterCategories}
|
||||
onChange={(value) => {
|
||||
const sortedValue = allAppGuideCategories.filter((category) =>
|
||||
value.includes(category)
|
||||
);
|
||||
setFilterCategories(sortedValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{keyword && (
|
||||
<GuideGroup
|
||||
hasCardBorder={hasCardBorder}
|
||||
guides={filteredMetadata}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
)}
|
||||
{!keyword && (
|
||||
<OverlayScrollbar className={styles.groups}>
|
||||
{(filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map(
|
||||
(category) =>
|
||||
structuredMetadata[category].length > 0 && (
|
||||
<GuideGroup
|
||||
key={category}
|
||||
hasCardBorder={hasCardBorder}
|
||||
categoryName={t(`categories.${category}`)}
|
||||
guides={structuredMetadata[category]}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
)}
|
||||
{selectedGuide?.target !== 'API' && showCreateForm && (
|
||||
<CreateForm defaultCreateType={selectedGuide?.target} onClose={onCloseCreateForm} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuideLibrary;
|
|
@ -0,0 +1,38 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-base);
|
||||
height: 100vh;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
inset: auto 0 0 0;
|
||||
padding: _.unit(4) _.unit(8);
|
||||
background-color: var(--color-layer-1);
|
||||
box-shadow: var(--shadow-2-reversed);
|
||||
z-index: 1;
|
||||
|
||||
.text {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text);
|
||||
margin-left: _.unit(62.5);
|
||||
margin-right: _.unit(4);
|
||||
@include _.multi-line-ellipsis(2);
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import Button from '@/ds-components/Button';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import CreateForm from '../CreateForm';
|
||||
import GuideHeader from '../GuideHeader';
|
||||
import GuideLibrary from '../GuideLibrary';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function GuideLibraryModal({ isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.applications.guide' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
return (
|
||||
<Modal
|
||||
shouldCloseOnEsc
|
||||
isOpen={isOpen}
|
||||
className={modalStyles.fullScreen}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<GuideHeader onClose={onClose} />
|
||||
<GuideLibrary hasFilter className={styles.content} />
|
||||
<nav className={styles.actionBar}>
|
||||
<span className={styles.text}>{t('do_not_need_tutorial')}</span>
|
||||
<Button
|
||||
className={styles.button}
|
||||
size="large"
|
||||
title="applications.guide.create_without_framework"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
setShowCreateForm(true);
|
||||
}}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
{showCreateForm && (
|
||||
<CreateForm
|
||||
onClose={(newApp) => {
|
||||
if (newApp) {
|
||||
navigate(`/applications/${newApp.id}`);
|
||||
}
|
||||
setShowCreateForm(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuideLibraryModal;
|
|
@ -34,7 +34,9 @@
|
|||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
padding: _.unit(4);
|
||||
background-color: var(--color-bg-float);
|
||||
background-color: var(--color-layer-1);
|
||||
box-shadow: var(--shadow-2-reversed);
|
||||
z-index: 1;
|
||||
|
||||
.layout {
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -19,7 +19,7 @@ import TextLink from '@/ds-components/TextLink';
|
|||
import useCustomDomain from '@/hooks/use-custom-domain';
|
||||
import DetailsSummary from '@/mdx-components/DetailsSummary';
|
||||
|
||||
import GuideHeaderV2 from '../GuideHeaderV2';
|
||||
import GuideHeader from '../GuideHeader';
|
||||
import StepsSkeleton from '../StepsSkeleton';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -39,16 +39,17 @@ type GuideContextType = {
|
|||
export const GuideContext = createContext<GuideContextType>({} as GuideContextType);
|
||||
|
||||
type Props = {
|
||||
guideId: string;
|
||||
app?: Application;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function GuideV2({ app, isCompact, onClose }: Props) {
|
||||
function GuideV2({ guideId, app, isCompact, onClose }: Props) {
|
||||
const { tenantEndpoint } = useContext(AppDataContext);
|
||||
const { data: customDomain } = useCustomDomain();
|
||||
const isCustomDomainActive = customDomain?.status === DomainStatus.Active;
|
||||
const guide = guides.find(({ id }) => id === 'web-gpt-plugin');
|
||||
const guide = guides.find(({ id }) => id === guideId);
|
||||
|
||||
if (!app || !guide) {
|
||||
throw new Error('Invalid app or guide');
|
||||
|
@ -73,7 +74,7 @@ function GuideV2({ app, isCompact, onClose }: Props) {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<GuideHeaderV2 appName={app.name} isCompact={isCompact} onClose={onClose} />
|
||||
<GuideHeader isCompact={isCompact} onClose={onClose} />
|
||||
<div className={styles.content}>
|
||||
<GuideContext.Provider value={memorizedContext}>
|
||||
<MDXProvider
|
||||
|
@ -102,7 +103,7 @@ function GuideV2({ app, isCompact, onClose }: Props) {
|
|||
</GuideContext.Provider>
|
||||
<nav className={styles.actionBar}>
|
||||
<div className={styles.layout}>
|
||||
<Button size="large" title="cloud.sie.finish_and_done" type="primary" />
|
||||
<Button size="large" title="applications.guide.finish_and_done" type="primary" />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -7,3 +7,20 @@
|
|||
.applicationName {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.guideLibraryContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--color-layer-1);
|
||||
border-radius: 12px;
|
||||
padding: _.unit(6) 0;
|
||||
margin: _.unit(4) 0;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.library {
|
||||
padding: 0 _.unit(6);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import { withAppInsights } from '@logto/app-insights/react';
|
||||
import type { Application } from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Plus from '@/assets/icons/plus.svg';
|
||||
import ApplicationIcon from '@/components/ApplicationIcon';
|
||||
import ItemPreview from '@/components/ItemPreview';
|
||||
import ListPage from '@/components/ListPage';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import Button from '@/ds-components/Button';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import Table from '@/ds-components/Table';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import * as pageLayout from '@/scss/page-layout.module.scss';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { buildUrl } from '@/utils/url';
|
||||
|
||||
import ApplicationsPlaceholder from './components/ApplicationsPlaceholder';
|
||||
import CreateForm from './components/CreateForm';
|
||||
import GuideLibrary from './components/GuideLibrary';
|
||||
import GuideLibraryModal from './components/GuideLibraryModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const pageSize = defaultPageSize;
|
||||
|
@ -39,7 +44,6 @@ function Applications() {
|
|||
const { match, navigate } = useTenantPathname();
|
||||
const isCreating = match(createApplicationPathname);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [defaultCreateType, setDefaultCreateType] = useState<ApplicationType>();
|
||||
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
|
||||
page: 1,
|
||||
});
|
||||
|
@ -55,89 +59,87 @@ function Applications() {
|
|||
const [applications, totalCount] = data ?? [];
|
||||
|
||||
return (
|
||||
<ListPage
|
||||
title={{
|
||||
title: 'applications.title',
|
||||
subtitle: 'applications.subtitle',
|
||||
}}
|
||||
pageMeta={{ titleKey: 'applications.title' }}
|
||||
createButton={{
|
||||
title: 'applications.create',
|
||||
onClick: () => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
},
|
||||
}}
|
||||
table={{
|
||||
rowGroups: [{ key: 'applications', data: applications }],
|
||||
rowIndexKey: 'id',
|
||||
isLoading,
|
||||
errorMessage: error?.body?.message ?? error?.message,
|
||||
columns: [
|
||||
{
|
||||
title: t('applications.application_name'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 6,
|
||||
render: ({ id, name, type }) => (
|
||||
<ItemPreview
|
||||
title={name}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
icon={<ApplicationIcon className={styles.icon} type={type} />}
|
||||
to={buildDetailsPathname(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('applications.app_id'),
|
||||
dataIndex: 'id',
|
||||
colSpan: 10,
|
||||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
||||
},
|
||||
],
|
||||
placeholder: (
|
||||
<ApplicationsPlaceholder
|
||||
onSelect={async (createType) => {
|
||||
setDefaultCreateType(createType);
|
||||
<div className={pageLayout.container}>
|
||||
<PageMeta titleKey="applications.title" />
|
||||
<div className={pageLayout.headline}>
|
||||
<CardTitle title="applications.title" subtitle="applications.subtitle" />
|
||||
{!!totalCount && (
|
||||
<Button
|
||||
icon={<Plus />}
|
||||
type="primary"
|
||||
size="large"
|
||||
title="applications.create"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
rowClickHandler: ({ id }) => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
},
|
||||
onRetry: async () => mutate(undefined, true),
|
||||
pagination: {
|
||||
page,
|
||||
totalCount,
|
||||
pageSize,
|
||||
onChange: (page) => {
|
||||
updateSearchParameters({ page });
|
||||
},
|
||||
},
|
||||
}}
|
||||
widgets={
|
||||
<CreateForm
|
||||
defaultCreateType={defaultCreateType}
|
||||
isOpen={isCreating}
|
||||
onClose={async (newApp) => {
|
||||
setDefaultCreateType(undefined);
|
||||
if (newApp) {
|
||||
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
pathname: applicationsPathname,
|
||||
search,
|
||||
});
|
||||
)}
|
||||
</div>
|
||||
{!isLoading && !applications?.length && (
|
||||
<OverlayScrollbar className={styles.guideLibraryContainer}>
|
||||
<CardTitle
|
||||
className={styles.title}
|
||||
title="applications.guide.header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<GuideLibrary hasCardBorder className={styles.library} />
|
||||
</OverlayScrollbar>
|
||||
)}
|
||||
{(isLoading || !!applications?.length) && (
|
||||
<Table
|
||||
isLoading={isLoading}
|
||||
className={pageLayout.table}
|
||||
rowGroups={[{ key: 'applications', data: applications }]}
|
||||
rowIndexKey="id"
|
||||
errorMessage={error?.body?.message ?? error?.message}
|
||||
columns={[
|
||||
{
|
||||
title: t('applications.application_name'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 6,
|
||||
render: ({ id, name, type }) => (
|
||||
<ItemPreview
|
||||
title={name}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
icon={<ApplicationIcon className={styles.icon} type={type} />}
|
||||
to={buildDetailsPathname(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('applications.app_id'),
|
||||
dataIndex: 'id',
|
||||
colSpan: 10,
|
||||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
||||
},
|
||||
]}
|
||||
rowClickHandler={({ id }) => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
}}
|
||||
pagination={{
|
||||
page,
|
||||
totalCount,
|
||||
pageSize,
|
||||
onChange: (page) => {
|
||||
updateSearchParameters({ page });
|
||||
},
|
||||
}}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<GuideLibraryModal
|
||||
isOpen={isCreating}
|
||||
onClose={() => {
|
||||
navigate({
|
||||
pathname: applicationsPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ export const applicationTypeAndSdkTypeMappings = Object.freeze({
|
|||
* All application guide categories, including all 4 existing application types,
|
||||
* plus the "featured" category.
|
||||
*/
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
export const allAppGuideCategories = Object.freeze([
|
||||
'featured',
|
||||
'Traditional',
|
||||
|
@ -50,4 +49,3 @@ export type AppGuideCategory = (typeof allAppGuideCategories)[number];
|
|||
* E.g. `{'featured': [...], 'Traditional': [...], 'SPA': [...], 'Native': [...]}`
|
||||
*/
|
||||
export type StructuredAppGuideMetadata = Record<AppGuideCategory, readonly Guide[]>;
|
||||
/* eslint-enable import/no-unused-modules */
|
||||
|
|
|
@ -37,14 +37,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Zum Beispielprojekt',
|
||||
header_description:
|
||||
'Folge der Schritt-für-Schritt-Anleitung, um die Anwendung zu integrieren, oder klick auf die rechte Schaltfläche, um unser Beispielprojekt zu erhalten',
|
||||
title: 'Die Anwendung wurde erfolgreich erstellt',
|
||||
subtitle:
|
||||
'Folge nun den folgenden Schritten, um deine App-Einstellungen abzuschließen. Bitte wähle den SDK-Typ aus, um fortzufahren.',
|
||||
description_by_sdk:
|
||||
'Diese Schnellstart-Anleitung zeigt, wie man Logto in die {{sdk}} App integriert',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Fertig und erledigt',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -36,14 +36,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial',
|
||||
modal_header_title: 'Start with SDK and guides',
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.',
|
||||
start_building: 'Start Building',
|
||||
categories: {
|
||||
featured: 'Popular and for you',
|
||||
Traditional: 'Traditional web app',
|
||||
SPA: 'Single page app',
|
||||
Native: 'Native',
|
||||
MachineToMachine: 'Machine-to-machine',
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework',
|
||||
placeholder: 'Search for framework',
|
||||
},
|
||||
get_sample_file: 'Get Sample',
|
||||
header_description:
|
||||
'Follow a step by step guide to integrate your application or click the right button to get our sample project',
|
||||
title: 'The application has been successfully created',
|
||||
subtitle:
|
||||
'Now follow the steps below to finish your app settings. Please select the SDK type to continue.',
|
||||
description_by_sdk:
|
||||
'This quick start guide demonstrates how to integrate Logto into {{sdk}} app',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide',
|
||||
create_without_framework: 'Create app without framework',
|
||||
finish_and_done: 'Finish and done',
|
||||
cannot_find_guide: "Can't find your guide?",
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for',
|
||||
|
|
|
@ -37,14 +37,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Obtener muestra',
|
||||
header_description:
|
||||
'Sigue una guía paso a paso para integrar tu aplicación o haz clic en el botón adecuado para obtener nuestro proyecto de muestra',
|
||||
title: 'La aplicación se ha creado correctamente',
|
||||
subtitle:
|
||||
'Sigue los pasos siguientes para completar la configuración de tu aplicación. Por favor, selecciona el tipo de SDK para continuar.',
|
||||
description_by_sdk:
|
||||
'Esta guía de inicio rápido muestra cómo integrar Logto en la aplicación {{sdk}}',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Finalizar y hecho',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -38,14 +38,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Obtenir un exemple',
|
||||
header_description:
|
||||
'Suivez un guide étape par étape pour intégrer votre application ou cliquez sur le bouton de droite pour obtenir notre exemple de projet.',
|
||||
title: "L'application a été créée avec succès",
|
||||
subtitle:
|
||||
'Suivez maintenant les étapes ci-dessous pour terminer la configuration de votre application. Veuillez sélectionner le type de SDK pour continuer.',
|
||||
description_by_sdk:
|
||||
"Ce guide de démarrage rapide montre comment intégrer Logto dans l'application {{sdk}}.",
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Terminer et terminé',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -37,13 +37,29 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Scarica Esempio',
|
||||
header_description:
|
||||
'Segui la guida passo passo per integrare la tua applicazione o clicca il pulsante corretto per scaricare il nostro progetto di esempio',
|
||||
title: "L'applicazione è stata creata con successo",
|
||||
subtitle:
|
||||
'Ora segui i passi di seguito per completare le impostazioni della tua app. Seleziona il tipo di SDK per continuare.',
|
||||
description_by_sdk: "Questa guida rapida illustra come integrare Logto in un'app {{sdk}}",
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Completato e fatto',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -36,14 +36,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'サンプルを取得する',
|
||||
header_description:
|
||||
'アプリケーションを統合するためのステップバイステップガイドに従うか、サンプルプロジェクトを取得するための適切なボタンをクリックしてください。',
|
||||
title: 'アプリケーションが正常に作成されました',
|
||||
subtitle:
|
||||
'以下の手順に従ってアプリの設定を完了してください。SDKタイプを選択して続行してください。',
|
||||
description_by_sdk:
|
||||
'このクイックスタートガイドでは、{{sdk}}アプリにLogtoを統合する方法を説明します。',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: '完了',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -36,12 +36,28 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: '예제 찾기',
|
||||
header_description:
|
||||
'단계별 가이드에 따라 어플리케이션을 연동하거나, 오른쪽 버튼을 클릭하여 샘플 프로젝트를 받아 보세요.',
|
||||
title: '어플리케이션이 생성되었어요.',
|
||||
subtitle: '앱 설정을 마치기 위해 아래 단계를 따라주세요. SDK 종류를 선택해 주세요.',
|
||||
description_by_sdk: '아래 과정을 따라서 Logto를 {{sdk}} 앱과 빠르게 연동해 보세요.',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: '끝내기',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -37,14 +37,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Pobierz przykład',
|
||||
header_description:
|
||||
'Postępuj zgodnie z przewodnikiem krok po kroku, aby zintegrować swoją aplikację lub kliknij prawy przycisk, aby pobrać nasz przykładowy projekt',
|
||||
title: 'Aplikacja została pomyślnie utworzona',
|
||||
subtitle:
|
||||
'Teraz postępuj zgodnie z poniższymi krokami, aby zakończyć konfigurację aplikacji. Wybierz typ SDK, aby kontynuować.',
|
||||
description_by_sdk:
|
||||
'Ten przewodnik po szybkim rozpoczęciu demonstruje, jak zintegrować Logto z aplikacją {{sdk}}',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Zakończ i zrobione',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -37,14 +37,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Obter amostra',
|
||||
header_description:
|
||||
'Siga um guia passo a passo para integrar seu aplicativo ou clique no botão direito para obter nosso projeto de amostra',
|
||||
title: 'O aplicativo foi criado com sucesso',
|
||||
subtitle:
|
||||
'Agora siga as etapas abaixo para concluir as configurações do aplicativo. Selecione o tipo de SDK para continuar.',
|
||||
description_by_sdk:
|
||||
'Este guia de início rápido demonstra como integrar o Logto ao aplicativo {{sdk}}',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Concluído',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -36,13 +36,29 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Obter amostra',
|
||||
header_description:
|
||||
'Siga um guia passo a passo para integrar a sua aplicação ou clique com o botão direito para obter nosso projeto de amostra',
|
||||
title: 'A aplicação foi criada com sucesso',
|
||||
subtitle:
|
||||
'Agora siga as etapas abaixo para concluir as configurações da aplicação. Selecione o tipo de SDK para continuar.',
|
||||
description_by_sdk: 'Este guia de início rápido demonstra como integrar o Logto em {{sdk}}',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Finalizar e concluir',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -36,14 +36,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Получить образец',
|
||||
header_description:
|
||||
'Следуйте пошаговому руководству, чтобы интегрировать ваше приложение, или нажимите правильную кнопку, чтобы получить наш образец проекта',
|
||||
title: 'Приложение успешно создано',
|
||||
subtitle:
|
||||
'Теперь следуйте инструкциям ниже, чтобы завершить настройку приложения. Выберите тип SDK, чтобы продолжить.',
|
||||
description_by_sdk:
|
||||
'Это быстрое руководство демонстрирует, как интегрировать Logto в {{sdk}} приложение',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Завершить и готово',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -37,14 +37,30 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: 'Örnek Gör',
|
||||
header_description:
|
||||
'Uygulamanızı entegre etmek için adım adım kılavuzu izleyin veya örnek projemizi almak için sağ düğmeye tıklayınız',
|
||||
title: 'Uygulama başarıyla oluşturuldu',
|
||||
subtitle:
|
||||
'Şimdi uygulama ayarlarınızı tamamlamak için aşağıdaki adımları izleyiniz. Lütfen devam etmek için SDK türünü seçiniz.',
|
||||
description_by_sdk:
|
||||
'Bu hızlı başlangıç kılavuzu, Logtoyu {{sdk}} uygulamasına nasıl entegre edeceğinizi gösterir',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: 'Bitir ve tamamlandı',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -34,12 +34,28 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: '获取示例',
|
||||
header_description:
|
||||
'参考如下教程,将 Logto 集成到你的应用中。你也可以点击右侧按钮,获取我们为你准备好的示例工程。',
|
||||
title: '应用创建成功',
|
||||
subtitle: '参考以下步骤完成你的应用设置。首先,选择你要使用的 SDK 类型:',
|
||||
description_by_sdk: '本教程向你演示如何在 {{sdk}} 应用中集成 Logto 登录功能',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: '完成并结束',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -34,12 +34,28 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: '獲取示例',
|
||||
header_description:
|
||||
'參考如下教程,將 Logto 集成到你的應用中。你也可以點擊右側按鈕,獲取我們為你準備好的示例工程。',
|
||||
title: '應用創建成功',
|
||||
subtitle: '參考以下步驟完成你的應用設置。首先,選擇你要使用的 SDK 類型:',
|
||||
description_by_sdk: '本教程向你演示如何在 {{sdk}} 應用中集成 Logto 登錄功能',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: '完成並結束',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
|
@ -34,12 +34,28 @@ const applications = {
|
|||
},
|
||||
},
|
||||
guide: {
|
||||
header_title: 'Select a framework or tutorial', // UNTRANSLATED
|
||||
modal_header_title: 'Start with SDK and guides', // UNTRANSLATED
|
||||
header_subtitle: 'Jumpstart your app development process with our pre-built SDK and tutorials.', // UNTRANSLATED
|
||||
start_building: 'Start Building', // UNTRANSLATED
|
||||
categories: {
|
||||
featured: 'Popular and for you', // UNTRANSLATED
|
||||
Traditional: 'Traditional web app', // UNTRANSLATED
|
||||
SPA: 'Single page app', // UNTRANSLATED
|
||||
Native: 'Native', // UNTRANSLATED
|
||||
MachineToMachine: 'Machine-to-machine', // UNTRANSLATED
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter framework', // UNTRANSLATED
|
||||
placeholder: 'Search for framework', // UNTRANSLATED
|
||||
},
|
||||
get_sample_file: '獲取示例',
|
||||
header_description:
|
||||
'參考如下教程,將 Logto 集成到你的應用中。你也可以點擊右側按鈕,獲取我們為你準備好的示例工程。',
|
||||
title: '應用創建成功',
|
||||
subtitle: '參考以下步驟完成你的應用設置。首先,選擇你要使用的 SDK 類型:',
|
||||
description_by_sdk: '本教程向你演示如何在 {{sdk}} 應用中集成 Logto 登入功能',
|
||||
do_not_need_tutorial:
|
||||
'If you don’t need a tutorial, you can continue without a framework guide', // UNTRANSLATED
|
||||
create_without_framework: 'Create app without framework', // UNTRANSLATED
|
||||
finish_and_done: '完成並結束',
|
||||
cannot_find_guide: "Can't find your guide?", // UNTRANSLATED
|
||||
describe_guide_looking_for: 'Describe the guide you are looking for', // UNTRANSLATED
|
||||
|
|
Loading…
Add table
Reference in a new issue