0
Fork 0
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:
Charles Zhao 2023-08-16 15:18:24 +08:00 committed by GitHub
parent 1fe7d1aa33
commit d0f91d5d37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 888 additions and 450 deletions

View file

@ -26,7 +26,7 @@ type Props<
subHeader?: ReactNode;
table: TableProps<TFieldValues, TName>;
/** @deprecated Need refactor. */
widgets: ReactNode;
widgets?: ReactNode;
className?: string;
};

View file

@ -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">

View file

@ -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);
}

View file

@ -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}`);

View file

@ -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);
}
}
}
}

View file

@ -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;

View file

@ -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 })}

View file

@ -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>
);

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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>
);
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -83,5 +83,4 @@ const useAppGuideMetadata = (): [
return [getFilteredMetadata, getStructuredMetadata];
};
// eslint-disable-next-line import/no-unused-modules
export default useAppGuideMetadata;

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>
);
}

View file

@ -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 */

View file

@ -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 dont 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

View file

@ -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 dont 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',

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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

View file

@ -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 dont 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