0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Merge pull request #432 from logto-io/charles-log-1919-dynamically-load-get-started-markdown-metadata

feat(core): provide a means to fetch get-started markdown metadata
This commit is contained in:
Charles Zhao 2022-03-28 14:56:15 +08:00 committed by GitHub
commit a08abfd4c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 623 additions and 128 deletions

View file

@ -8,10 +8,11 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"precommit": "lint-staged",
"start": "parcel src/index.html",
"dev": "PORT=5002 parcel src/index.html --public-url /console --no-hmr --no-cache",
"copyfiles": "copyfiles -u 1 public/**/*.* dist",
"start": "pnpm copyfiles && parcel src/index.html",
"dev": "pnpm copyfiles && PORT=5002 parcel src/index.html --public-url /console --no-hmr --no-cache",
"check": "tsc --noEmit",
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --public-url /console",
"build": "pnpm check && rm -rf dist && pnpm copyfiles && parcel build src/index.html --no-autoinstall --public-url /console",
"lint": "eslint --ext .ts --ext .tsx src",
"stylelint": "stylelint \"src/**/*.scss\""
},
@ -50,6 +51,7 @@
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-modal": "^3.13.1",
"copyfiles": "^2.4.1",
"eslint": "^8.10.0",
"lint-staged": "^11.1.1",
"parcel": "^2.3.2",

View file

@ -0,0 +1,3 @@
{
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
}

View file

@ -0,0 +1,34 @@
---
title: Install Logto SDK
subtitle: 1 step
---
## Option 1: Install your SDK dependency
Run the CLI command under your project's root directory.
```
// installation with npm
npm install @logto/react --save
// installation with yarn
yarn add @logto/react
// installation with pnpm
pnpm install @logto/react --save
```
## Option 2: Add script tag to your HTML
```
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
```
## Option 3: Fork your own from github
```
git clone https://github.com/logto-io/js.git
```
```
pnpm build
```

View file

@ -0,0 +1,35 @@
---
title: Initiate LogtoClient
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
---
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -0,0 +1,47 @@
---
title: Sign In
subtitle: 2 steps
---
## Step 1: Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```multitextinput
Redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
## Step 2: Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -0,0 +1,26 @@
---
title: Sign Out
subtitle: 1 steps
---
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```multitextinput
Post sign out redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -0,0 +1,15 @@
---
title: Advanced Settings & Documentation Links
subtitle: 2 steps
---
## Step 1: Advanced Settings
Go to application details page and switch to advanced settings tab
## Step 2: Now check out the documentation links below
[- SDK Documentation](https://link-url-here.org)
[- OIDC Documentation](https://link-url-here.org)
[- Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -0,0 +1,3 @@
{
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
}

View file

@ -0,0 +1,34 @@
---
title: 安装 Logto SDK
subtitle: 1 step
---
## 方案1: 安装 SDK 依赖包
Run the CLI command under your project's root directory.
```
// installation with npm
npm install @logto/react --save
// installation with yarn
yarn add @logto/react
// installation with pnpm
pnpm install @logto/react --save
```
## 方案2: Add script tag to your HTML
```
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
```
## 方案3: Fork your own from github
```
git clone https://github.com/logto-io/js.git
```
```
pnpm build
```

View file

@ -0,0 +1,35 @@
---
title: 初始化 LogtoClient
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
---
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -0,0 +1,47 @@
---
title: 登录
subtitle: 2 steps
---
## Step 1: Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```multitextinput
Redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
## Step 2: Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -0,0 +1,26 @@
---
title: 登出
subtitle: 1 steps
---
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```multitextinput
Post sign out redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -0,0 +1,15 @@
---
title: 高级设置以及相关文档链接
subtitle: 2 steps
---
## Step 1: Advanced Settings
Go to application details page and switch to advanced settings tab
## Step 2: Now check out the documentation links below
[- SDK Documentation](https://link-url-here.org)
[- OIDC Documentation](https://link-url-here.org)
[- Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -38,6 +38,7 @@ $font-family: 'SF UI Text', 'SF Pro Display', sans-serif;
--font-heading-small: 600 24px/32px #{$font-family};
--font-heading: 600 16px/24px #{$font-family};
--font-body: normal 16px/22px #{$font-family};
--font-body-1: normal 16px/24px #{$font-family};
--font-body-2: normal 14px/20px #{$font-family};
--font-small-text: normal 12px/16px #{$font-family};
--font-caption: normal 14px/20px #{$font-family};

View file

@ -4,5 +4,4 @@
background: var(--color-on-primary);
border-radius: _.unit(4);
padding: _.unit(6);
min-height: 100%;
}

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import React, { forwardRef, LegacyRef, ReactNode } from 'react';
import * as styles from './index.module.scss';
@ -8,8 +8,14 @@ type Props = {
className?: string;
};
const Card = ({ children, className }: Props) => {
return <div className={classNames(styles.card, className)}>{children}</div>;
const Card = (props: Props, reference?: LegacyRef<HTMLDivElement>) => {
const { children, className } = props;
return (
<div ref={reference} className={classNames(styles.card, className)}>
{children}
</div>
);
};
export default Card;
export default forwardRef<HTMLDivElement, Props>(Card);

View file

@ -1,5 +1,5 @@
import { AdminConsoleKey } from '@logto/phrases';
import React, { ReactNode } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './index.module.scss';
@ -9,32 +9,18 @@ type Props = {
subtitle?: AdminConsoleKey;
};
type RawProps = {
title: ReactNode;
subtitle?: ReactNode;
};
/**
* Do not use this component directly, unless you do not want to use translation.
*/
export const RawCardTitle = ({ title, subtitle }: RawProps) => (
<div>
<div className={styles.title}>{title}</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</div>
);
/**
* Always use this component to render CardTitle, with built-in i18n support.
*/
const CardTitle = ({ title, subtitle }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const props: RawProps = {
title: t(title),
subtitle: subtitle ? t(subtitle) : undefined,
};
return <RawCardTitle {...props} />;
return (
<div>
<div className={styles.title}>{t(title)}</div>
{subtitle && <div className={styles.subtitle}>{t(subtitle)}</div>}
</div>
);
};
export default CardTitle;

View file

@ -19,7 +19,7 @@ export class RequestError extends Error {
const toastError = async (response: Response) => {
try {
const data = (await response.json()) as RequestErrorBody;
const data = await response.json<RequestErrorBody>();
toast.error(data.message || t('admin_console.errors.unknown_server_error'));
} catch {
toast.error(t('admin_console.errors.unknown_server_error'));

View file

@ -16,7 +16,7 @@ const useSwrFetcher = () => {
} catch (error: unknown) {
if (error instanceof HTTPError) {
const { response } = error;
const metadata = (await response.json()) as RequestErrorBody;
const metadata = await response.json<RequestErrorBody>();
throw new RequestError(metadata);
}
throw error;

View file

@ -0,0 +1,31 @@
import React, { SVGProps } from 'react';
export const ArrowDown = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M12.71 15.5402L18.36 9.88023C18.4537 9.78726 18.5281 9.67666 18.5789 9.5548C18.6296 9.43294 18.6558 9.30224 18.6558 9.17023C18.6558 9.03821 18.6296 8.90751 18.5789 8.78565C18.5281 8.66379 18.4537 8.55319 18.36 8.46023C18.1726 8.27398 17.9191 8.16943 17.655 8.16943C17.3908 8.16943 17.1373 8.27398 16.95 8.46023L11.95 13.4102L6.99996 8.46023C6.8126 8.27398 6.55915 8.16943 6.29496 8.16943C6.03078 8.16943 5.77733 8.27398 5.58996 8.46023C5.49548 8.55284 5.42031 8.66329 5.36881 8.78516C5.31731 8.90704 5.29051 9.03792 5.28996 9.17023C5.29051 9.30253 5.31731 9.43342 5.36881 9.55529C5.42031 9.67717 5.49548 9.78761 5.58996 9.88023L11.24 15.5402C11.3336 15.6417 11.4473 15.7227 11.5738 15.7781C11.7003 15.8336 11.8369 15.8622 11.975 15.8622C12.1131 15.8622 12.2497 15.8336 12.3762 15.7781C12.5027 15.7227 12.6163 15.6417 12.71 15.5402Z" />
</svg>
);
};
export const ArrowUp = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M11.29 8.45977L5.64004 14.1198C5.54631 14.2127 5.47191 14.3233 5.42115 14.4452C5.37038 14.5671 5.34424 14.6978 5.34424 14.8298C5.34424 14.9618 5.37038 15.0925 5.42115 15.2144C5.47191 15.3362 5.54631 15.4468 5.64004 15.5398C5.8274 15.726 6.08085 15.8306 6.34504 15.8306C6.60922 15.8306 6.86267 15.726 7.05004 15.5398L12.05 10.5898L17 15.5398C17.1874 15.726 17.4409 15.8306 17.705 15.8306C17.9692 15.8306 18.2227 15.726 18.41 15.5398C18.5045 15.4472 18.5797 15.3367 18.6312 15.2148C18.6827 15.093 18.7095 14.9621 18.71 14.8298C18.7095 14.6975 18.6827 14.5666 18.6312 14.4447C18.5797 14.3228 18.5045 14.2124 18.41 14.1198L12.76 8.45977C12.6664 8.35827 12.5527 8.27726 12.4262 8.22185C12.2997 8.16645 12.1631 8.13784 12.025 8.13784C11.8869 8.13784 11.7503 8.16645 11.6238 8.22185C11.4973 8.27726 11.3837 8.35827 11.29 8.45977Z" />
</svg>
);
};

View file

@ -0,0 +1,16 @@
import React, { SVGProps } from 'react';
const Tick = (props: SVGProps<SVGSVGElement>) => (
<svg
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M15.7016 0.216515C16.0686 0.531016 16.1016 1.07279 15.7755 1.42661L6.29399 11.7123C6.12531 11.8953 5.88353 12 5.62963 12C5.37573 12 5.13395 11.8953 4.96527 11.7123L0.224534 6.56946C-0.101615 6.21565 -0.0685652 5.67387 0.298352 5.35937C0.66527 5.04487 1.22711 5.07674 1.55326 5.43055L5.62963 9.85269L14.4467 0.287697C14.7729 -0.0661164 15.3347 -0.0979856 15.7016 0.216515Z" />
</svg>
);
export default Tick;

View file

@ -0,0 +1,3 @@
interface Body {
json<T>(): Promise<T>;
}

View file

@ -6,6 +6,16 @@
background-color: var(--color-main-background);
height: 100vh;
.title {
font: var(--font-title-large);
color: var(--color-component-text);
}
.subtitle {
font: var(--font-body-1);
color: var(--color-component-caption);
}
.header {
display: flex;
align-items: center;
@ -17,16 +27,6 @@
margin-left: _.unit(4);
}
.title {
font: var(--font-title-large);
color: var(--color-component-text);
}
.subtitle {
font: var(--font-body-2);
color: var(--color-component-caption);
}
button + button {
margin-left: _.unit(4);
}
@ -45,26 +45,28 @@
width: 858px;
padding: _.unit(6) _.unit(8);
display: flex;
align-items: center;
flex-direction: column;
scroll-margin: _.unit(6);
&.selector {
display: block;
padding: _.unit(8);
margin-top: _.unit(4);
.subtitle {
color: var(--color-component-text);
margin-top: _.unit(3);
}
.radio {
border-radius: _.unit(2);
width: 240px;
max-width: unset;
}
.buttonWrapper {
display: flex;
margin-top: _.unit(8);
}
&.folded {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 0 56px;
background: var(--color-neutral-variant-90);
@ -79,20 +81,58 @@
}
}
}
&.expanded {
padding: _.unit(8);
}
.cardHeader {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
> svg {
fill: var(--color-icon);
}
.index {
width: 32px;
height: 32px;
border-radius: 50%;
color: var(--color-primary);
background: var(--color-surface-5);
font: var(--font-label-large);
text-align: center;
margin-right: _.unit(4);
&.active {
color: var(--color-on-primary);
background: var(--color-primary);
}
&.completed {
background: var(--color-primary);
> svg {
fill: var(--color-on-primary);
}
}
}
}
.buttonWrapper {
display: flex;
justify-content: flex-end;
margin-top: _.unit(6);
}
}
.card + .card {
margin-top: _.unit(6);
}
.index {
width: 32px;
height: 32px;
border-radius: 50%;
color: var(--color-primary);
background: var(--color-surface-5);
font: var(--font-label-large);
text-align: center;
margin-right: _.unit(4);
.markdownContent {
margin-top: _.unit(6);
}
}

View file

@ -1,17 +1,23 @@
import classNames from 'classnames';
import React, { useMemo, useState } from 'react';
import i18next from 'i18next';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
// eslint-disable-next-line node/file-extension-in-import
import useSWRImmutable from 'swr/immutable';
import highFive from '@/assets/images/high-five.svg';
import tada from '@/assets/images/tada.svg';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import IconButton from '@/components/IconButton';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import Spacer from '@/components/Spacer';
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
import Close from '@/icons/Close';
import Tick from '@/icons/Tick';
import { SupportedJavascriptLibraries } from '@/types/applications';
import { parseMarkdownWithYamlFrontmatter } from '@/utilities/markdown';
import * as styles from './index.module.scss';
@ -20,10 +26,49 @@ type Props = {
onClose: () => void;
};
type DocumentFileNames = {
files: string[];
};
type Step = {
title?: string;
subtitle?: string;
metadata: string;
};
const GetStarted = ({ appName, onClose }: Props) => {
const [isLibrarySelectorFolded, setIsLibrarySelectorFolded] = useState(false);
const [libraryName, setLibraryName] = useState<string>(SupportedJavascriptLibraries.Angular);
const [libraryName, setLibraryName] = useState<string>(SupportedJavascriptLibraries.React);
const [activeStepIndex, setActiveStepIndex] = useState<number>(-1);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const publicPath = useMemo(
() => `/console/get-started/${libraryName}/${i18next.language}`,
[libraryName]
);
const { data: jsonData } = useSWRImmutable<DocumentFileNames>(`${publicPath}/index.json`);
const { data: steps } = useSWRImmutable<Step[]>(jsonData, async ({ files }: DocumentFileNames) =>
Promise.all(
files.map(async (fileName) => {
const response = await fetch(`${publicPath}/${fileName}`);
const markdownFile = await response.text();
return parseMarkdownWithYamlFrontmatter<Step>(markdownFile);
})
)
);
const stepReferences = useMemo(
() => Array.from({ length: steps?.length ?? 0 }).map(() => React.createRef<HTMLDivElement>()),
[steps?.length]
);
useEffect(() => {
if (activeStepIndex > -1) {
const activeStepReference = stepReferences[activeStepIndex];
activeStepReference?.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
}, [activeStepIndex, stepReferences]);
const onClickFetchSampleProject = () => {
window.open(
@ -36,34 +81,28 @@ const GetStarted = ({ appName, onClose }: Props) => {
() => (
<Card className={classNames(styles.card, styles.selector)}>
<img src={highFive} alt="success" />
<CardTitle
title="applications.get_started.title"
subtitle="applications.get_started.subtitle"
/>
<RadioGroup
name="libraryName"
value={libraryName}
onChange={(value) => {
setLibraryName(value);
}}
>
<div>
<div className={styles.title}>{t('applications.get_started.title')}</div>
<div className={styles.subtitle}>{t('applications.get_started.subtitle')}</div>
</div>
<RadioGroup name="libraryName" value={libraryName} onChange={setLibraryName}>
{Object.values(SupportedJavascriptLibraries).map((library) => (
<Radio key={library} className={styles.radio} title={library} value={library} />
))}
</RadioGroup>
<div className={styles.buttonWrapper}>
<Spacer />
<Button
type="primary"
title="general.next"
onClick={() => {
setIsLibrarySelectorFolded(true);
setActiveStepIndex(0);
}}
/>
</div>
</Card>
),
[libraryName]
[libraryName, t]
);
const librarySelectorFolded = useMemo(
@ -98,42 +137,70 @@ const GetStarted = ({ appName, onClose }: Props) => {
</div>
<div className={styles.content}>
{isLibrarySelectorFolded ? librarySelectorFolded : librarySelector}
{/* TO-DO: Dynamically render steps from markdown files */}
<Card className={styles.card}>
<div className={styles.index}>1</div>
<CardTitle
title="applications.get_started.title_step_1"
subtitle="applications.get_started.subtitle_step_1"
/>
</Card>
<Card className={styles.card}>
<div className={styles.index}>2</div>
<CardTitle
title="applications.get_started.title_step_2"
subtitle="applications.get_started.subtitle_step_2"
/>
</Card>
<Card className={styles.card}>
<div className={styles.index}>3</div>
<CardTitle
title="applications.get_started.title_step_3"
subtitle="applications.get_started.subtitle_step_3"
/>
</Card>
<Card className={styles.card}>
<div className={styles.index}>4</div>
<CardTitle
title="applications.get_started.title_step_4"
subtitle="applications.get_started.subtitle_step_4"
/>
</Card>
<Card className={styles.card}>
<div className={styles.index}>5</div>
<CardTitle
title="applications.get_started.title_step_5"
subtitle="applications.get_started.subtitle_step_5"
/>
</Card>
{steps?.map((step, index) => {
const { title, subtitle, metadata } = step;
const isExpanded = activeStepIndex === index;
const isCompleted = activeStepIndex > index;
const isLastStep = index === steps.length - 1;
// Steps in get-started must have "title" declared in the Yaml header of the markdown source file
if (!title) {
return null;
}
// TODO: add more styles to markdown renderer
// TODO: render form and input fields in steps
return (
<Card
key={title}
ref={stepReferences[index]}
className={classNames(styles.card, isExpanded && styles.expanded)}
>
<div
className={styles.cardHeader}
onClick={() => {
setIsLibrarySelectorFolded(true);
setActiveStepIndex(index);
}}
>
<div
className={classNames(
styles.index,
isExpanded && styles.active,
isCompleted && styles.completed
)}
>
{isCompleted ? <Tick /> : index + 1}
</div>
<div>
<div className={styles.title}>{title}</div>
<div className={styles.subtitle}>{subtitle}</div>
</div>
<Spacer />
{isExpanded ? <ArrowUp /> : <ArrowDown />}
</div>
{isExpanded && (
<>
<ReactMarkdown className={styles.markdownContent}>{metadata}</ReactMarkdown>
<div className={styles.buttonWrapper}>
<Button
type="primary"
title={`general.${isLastStep ? 'done' : 'next'}`}
onClick={() => {
if (isLastStep) {
// TO-DO: submit form
onClose();
} else {
setActiveStepIndex(index + 1);
}
}}
/>
</div>
</>
)}
</Card>
);
})}
</div>
</div>
);

View file

@ -77,6 +77,7 @@
--color-outline: var(--color-neutral-variant-50);
--color-disabled: var(--color-neutral-80);
--color-readonly: var(--color-neutral-50);
--color-icon: var(--color-neutral-50);
--color-border: var(--color-neutral-80);
--color-table-row-selected: rgba(25, 28, 29, 4%);
--color-code-comment: #66bb6a;

View file

@ -0,0 +1,39 @@
type MarkdownWithYamlFrontmatter<T> = {
metadata: string;
} & {
[K in keyof T]?: string;
};
/**
* A Yaml frontmatter is a series of variables that are defined at the top of the markdown file,
* that normally is not part of the text contents themselves, such as title, subtitle.
* Yaml frontmatter both starts and ends with three dashes (---), and valid Yaml syntax can be used
* in between the three dashes, to define the variables.
*/
export const parseMarkdownWithYamlFrontmatter = <T extends Record<string, string>>(
markdown: string
): MarkdownWithYamlFrontmatter<T> => {
const metaRegExp = new RegExp(/^---[\n\r](((?!---).|[\n\r])*)[\n\r]---$/m);
// "rawYamlHeader" is the full matching string, including the --- and ---
// "metadata" is the first capturing group, which is the string between the --- and ---
const [rawYamlHeader, yamlVariables] = metaRegExp.exec(markdown) ?? [];
if (!rawYamlHeader || !yamlVariables) {
return { metadata: markdown };
}
const keyValues = yamlVariables.split('\n');
// Converts a list of string like ["key1: value1", "key2: value2"] to { key1: "value1", key2: "value2" }
const frontmatter = Object.fromEntries<string>(
keyValues.map((keyValue) => {
const splitted = keyValue.split(':');
const [key, value] = splitted;
return [key ?? keyValue, value?.trim() ?? ''];
})
) as Record<keyof T, string>;
return { ...frontmatter, metadata: markdown.replace(rawYamlHeader, '').trim() };
};

View file

@ -7,6 +7,7 @@ const translation = {
retry: 'Try again',
confirm: 'Confirm',
cancel: 'Cancel',
done: 'Done',
},
sign_in: {
action: 'Sign In',
@ -96,16 +97,6 @@ const translation = {
'Now follow the steps below to finish your app settings. Please select the JS library to continue.',
description_by_library:
'This quickstart demonstrates how to add Logto to {{library}} application.',
title_step_1: 'Install Logto SDK',
subtitle_step_1: '3 options | lorem ipsum dolor sit amet',
title_step_2: 'Initiate LogtoClient',
subtitle_step_2: '1 step | lorem ipsum dolor sit amet',
title_step_3: 'Sign in',
subtitle_step_3: '1 step | lorem ipsum dolor sit amet',
title_step_4: 'Sign out',
subtitle_step_4: '1 step | lorem ipsum dolor sit amet',
title_step_5: 'Further readings',
subtitle_step_5: 'Tutorial and readings',
},
},
application_details: {

View file

@ -9,6 +9,7 @@ const translation = {
retry: '重试',
confirm: '确认',
cancel: '取消',
done: '完成',
},
sign_in: {
action: '登录',
@ -96,16 +97,6 @@ const translation = {
title: '恭喜!您的应用已成功创建。',
subtitle: '请参考以下步骤完成您的应用设置。首先,请选择您要使用的 Javascript 框架:',
description_by_library: '本教程向您演示如何在 {{library}} 应用中集成 Logto 登录功能',
title_step_1: '安装 Logto SDK',
subtitle_step_1: '3 options | lorem ipsum dolor sit amet',
title_step_2: '初始化得到 LogtoClient 实例',
subtitle_step_2: '1 step | lorem ipsum dolor sit amet',
title_step_3: 'Sign in',
subtitle_step_3: '1 step | lorem ipsum dolor sit amet',
title_step_4: 'Sign out',
subtitle_step_4: '1 step | lorem ipsum dolor sit amet',
title_step_5: '延伸阅读',
subtitle_step_5: 'Tutorial and readings',
},
},
application_details: {

View file

@ -36,6 +36,7 @@ importers:
'@types/react-dom': ^17.0.9
'@types/react-modal': ^3.13.1
classnames: ^2.3.1
copyfiles: ^2.4.1
csstype: ^3.0.11
eslint: ^8.10.0
i18next: ^21.6.12
@ -95,6 +96,7 @@ importers:
'@types/react': 17.0.37
'@types/react-dom': 17.0.11
'@types/react-modal': 3.13.1
copyfiles: 2.4.1
eslint: 8.10.0
lint-staged: 11.2.6
parcel: 2.3.2_postcss@8.4.6