Skip to content

Commit

Permalink
docs: english translation init (#19)
Browse files Browse the repository at this point in the history
* init

* fix

* fix
  • Loading branch information
minuukang authored Aug 19, 2024
1 parent e17e118 commit a91cbaf
Show file tree
Hide file tree
Showing 23 changed files with 1,506 additions and 26 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@

### Strong Type Support

By comparing the type of the current step with the type of the next step, you can safely manage only the necessary states.
By comparing the type of the current step with the next, you can ensure that only the required states are managed safely.

### State Management by History

Manage states by history, so you can easily manage back and forth.
Manage states based on history, making it easy to handle backward and forward navigation.

### Various Router Support

Supports browser history, react-router-dom, next.js, @react-navigation/native, etc.
Supports browser history, react-router-dom, next.js, @react-navigation/native, and more.

## Example

Expand Down
4 changes: 2 additions & 2 deletions docs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const withNextra = require('nextra')({
/** @type {import('next').NextConfig} */
module.exports = withNextra({
i18n: {
locales: ['ko'],
defaultLocale: 'ko',
locales: ['ko', 'en'],
defaultLocale: 'en',
},
});
29 changes: 23 additions & 6 deletions docs/src/components/UseFunnelCodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Tabs } from 'nextra/components';
import { useEffect, useRef } from 'react';

export const useFunnelPackages = ['next', 'react-router-dom', 'react-navigation-native', 'browser'];
export const useFunnelPackages = [
{
packageName: 'next',
packageTitle: 'Next.js page router',
},
{
packageName: 'react-router-dom',
packageTitle: 'react-router-dom',
},
{
packageName: 'react-navigation-native',
packageTitle: '@react-navigation/native',
},
{
packageName: 'browser',
packageTitle: 'browser history api',
},
];

export function UseFunnelCodeBlock({ children }: React.PropsWithChildren<unknown>) {
return (
<Tabs items={useFunnelPackages} storageKey="favorite-package">
<Tabs items={useFunnelPackages.map((p) => p.packageTitle)} storageKey="favorite-package">
{useFunnelPackages.map((item) => (
<Tabs.Tab key={item}>
<UseFunnelImportReplace packageName={item}>{children}</UseFunnelImportReplace>
<Tabs.Tab key={item.packageTitle}>
<UseFunnelImportReplace packageName={item.packageName}>{children}</UseFunnelImportReplace>
</Tabs.Tab>
))}
</Tabs>
Expand All @@ -33,8 +50,8 @@ function UseFunnelImportReplace({
const tokens = Array.from(line.querySelectorAll('[style*="token-string-expression"]'));
tokens.forEach((token) => {
for (const targetPackage of useFunnelPackages) {
if (token.textContent?.includes(targetPackage)) {
token.textContent = token.textContent.replace(targetPackage, packageName);
if (token.textContent?.includes(targetPackage.packageName)) {
token.textContent = token.textContent.replace(targetPackage.packageName, packageName);
}
}
});
Expand Down
13 changes: 13 additions & 0 deletions docs/src/pages/_meta.en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"index": {
"type": "page",
"display": "hidden",
"theme": {
"layout": "full"
}
},
"docs": {
"type": "page",
"title": "View Docs"
}
}
29 changes: 29 additions & 0 deletions docs/src/pages/docs/_meta.en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"--- introduction": {
"type": "separator",
"title": "Introduction"
},
"overview": "Overview",
"features": "Main Features",
"--- Guide": {
"type": "separator",
"title": "Guide"
},
"install": "Installation",
"get-started": "Getting Started",
"--- Reference": {
"type": "separator",
"title": "Reference"
},
"use-funnel": "useFunnel()",
"funnel-render": "funnel.Render Component",
"--- Advanced": {
"type": "separator",
"title": "Advanced"
},
"context-guide": "Step-by-Step Definition of context",
"sub-funnel": "Creating a Funnel within a Funnel",
"custom-router": "Creating a Custom Router",
"transition-event": "Define Transition Events",
"overlay": "Using Overlay"
}
256 changes: 256 additions & 0 deletions docs/src/pages/docs/context-guide.en.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { Tabs, Callout } from 'nextra/components'
import { UseFunnelCodeBlock, Keyword } from '@/components'

# Step-by-Step Definition of <Keyword>context</Keyword>

In `@use-funnel`, you need to define the type of <Keyword>context</Keyword> required at each step (<Keyword>step</Keyword>). Let's look at different ways to define <Keyword>context</Keyword>.

## Defining with generics

Define the type of <Keyword>context</Keyword> required for each <Keyword>step</Keyword> as a generic, and pass the object type with <Keyword>step</Keyword> as the key and the type of <Keyword>context</Keyword> as the value to the generics of [`useFunnel()`](./use-funnel.mdx).

<UseFunnelCodeBlock>
```tsx {3-8,11-15}
import { useFunnel } from "@use-funnel/next";

// 1. Nothing entered
type EmailInput = { email?: string; password?: string; other?: unknown }
// 2. Email entered
type PasswordInput = { email: string; password?: string; other?: unknown }
// 3. Email and password entered
type OtherInfoInput = { email: string; password: string; other?: unknown }

function MyFunnelApp() {
const funnel = useFunnel<{
EmailInput: EmailInput;
PasswordInput: PasswordInput;
OtherInfoInput: OtherInfoInput;
}>({
id: "how-to-define-step-contexts",
initial: {
step: "EmailInput",
context: {}
}
});

funnel.step === 'EmailInput' && typeof funnel.context.email // "string" | "undefined"
funnel.step === 'PasswordInput' && typeof funnel.context.email // "string"

funnel.step === 'PasswordInput' && typeof funnel.context.password // "string" | "undefined"
funnel.step === 'OtherInfoInput' && typeof funnel.context.password // "string"
// ...
}
```
</UseFunnelCodeBlock>

## Defining with steps object

You can define <Keyword>step</Keyword> through [`FunnelStepOption`](./use-funnel#funnelstepoption) of [`useFunnel()`](./use-funnel). Define <Keyword>context</Keyword> through `guard()` or `parse()`.

<UseFunnelCodeBlock>
```tsx {9-26,35-39}
import { useFunnel } from "@use-funnel/next";

type FormState = {
email?: string;
password?: string;
other?: unknown;
}

function EmailInput_guard (data: unknown): data is FormState {
return typeof data === 'object' && typeof data.email === 'string';
}

function PasswordInput_parse (data: unknown): FormState & { password: string } {
if (!(data != null && typeof data === 'object' && 'password' in data)) {
throw new Error('Invalid passwordInput data');
}
return data;
}

function OtherInfoInput_parse (data: unknown): FormState & { password: string; other: unknown } {
const parseData = PasswordInput_parse(data);
if (!('other' in data)) {
throw new Error('Invalid otherInfoInput data');
}
return parseData;
}

function MyFunnelApp() {
const funnel = useFunnel<FormState>({
id: "how-to-define-step-contexts",
initial: {
step: "EmailInput",
context: {}
},
steps: {
EmailInput: { guard: EmailInput_guard },
PasswordInput: { parse: PasswordInput_parse },
OtherInfoInput: { parse: OtherInfoInput_parse },
}
});

funnel.step === 'EmailInput' && typeof funnel.context.email // "string" | "undefined"
funnel.step === 'PasswordInput' && typeof funnel.context.email // "string"

funnel.step === 'PasswordInput' && typeof funnel.context.password // "string" | "undefined"
funnel.step === 'OtherInfoInput' && typeof funnel.context.password // "string"
// ...
}
```
</UseFunnelCodeBlock>

- `guard` (`function`, optional): A function that checks if the data is of the context type of that step.
- `parse` (`function`, optional): A function that converts data to the context type of that step.

<Callout>
If both are defined, `guard()` is executed first.
</Callout>

### `createFunnelSteps()` to define

You can create a <Keyword>step</Keyword> that changes a specific key of the initial <Keyword>context</Keyword> from optional to required when transitioning to the next <Keyword>step</Keyword> simply.

```tsx
declare function createFunnelSteps<FormState>(): {
extends: (name: string | string[], options?: { requiredKeys: string | string[] }) => this;
build: () => Record<string, FunnelStepOption>;
}
```

- `extends` (`function`): Extends a new funnel step.
- `name` (`string`): The name of the new funnel step.
- `options` (`object`, optional): Options for the new funnel step.
- `requiredKeys` (`string`): The key to be required in the initial context.

- `build` (`function`): Builds the funnel steps.
- Returns (`Record<string, FunnelStepOption>`): An object with the step name as the key and the options of each step as the value.

<UseFunnelCodeBlock>
```tsx {3-13, 22}
import { createFunnelSteps, useFunnel } from "@use-funnel/next";

type FormState = {
email?: string;
password?: string;
other?: unknown;
}

const steps = createFunnelSteps<FormState>()
.extends('EmailInput')
.extends('PasswordInput', { requiredKeys: 'email' })
.extends('OtherInfoInput', { requiredKeys: 'password' })
.build();

function MyFunnelApp() {
const funnel = useFunnel({
id: "create-funnel-steps",
initial: {
step: "EmailInput",
context: {}
},
steps
});

funnel.step === 'EmailInput' && typeof funnel.context.email // "string" | "undefined"
funnel.step === 'PasswordInput' && typeof funnel.context.email // "string"

funnel.step === 'PasswordInput' && typeof funnel.context.password // "string" | "undefined"
funnel.step === 'OtherInfoInput' && typeof funnel.context.password // "string"
// ...
}
```
</UseFunnelCodeBlock>

## Using Runtime Validator Packages

Use runtime validator packages to validate data and ensure type safety. Here are examples using `zod` and `superstruct`.

<Tabs items={['zod', 'superstruct']}>
<Tabs.Tab>
```tsx
import { z } from 'zod';

const emailInputSchema = z.object({
email: z.string(),
password: z.string(),
other: z.unknown()
}).partial();

const passwordInputSchema = emailInputSchema.required({ email: true });
const otherInfoInputSchema = passwordInputSchema.required({ password: true });

function MyFunnelApp() {
const funnel = useFunnel({
id: "zod-example",
steps: {
EmailInput: { parse: emailInputSchema.parse },
PasswordInput: { parse: passwordInputSchema.parse },
OtherInfoInput: { parse: otherInfoInputSchema.parse },
},
initial: {
step: "EmailInput",
context: {}
}
});

funnel.step === 'EmailInput' && typeof funnel.context.email // "string" | "undefined"
funnel.step === 'PasswordInput' && typeof funnel.context.email // "string"

funnel.step === 'PasswordInput' && typeof funnel.context.password // "string" | "undefined"
funnel.step === 'OtherInfoInput' && typeof funnel.context.password // "string"
// ...
}
```
</Tabs.Tab>
<Tabs.Tab>
```tsx
import { partial, object, string, unknown, assign, omit, pick, create } from 'superstruct';

const schema = object({
email: string(),
password: string(),
other: unknown()
})

const emailInputSchema = partial(schema);

const passwordInputSchema = assign(
omit(emailInputSchema, ['email']),
pick(schema, ['email'])
);

const otherInfoInputSchema = assign(
omit(passwordInputSchema, ['password']),
pick(schema, ['password'])
);

function MyFunnelApp() {
const funnel = useFunnel({
id: "superstruct-example",
steps: {
EmailInput: { parse: (data) => create(data, emailInputSchema) }
PasswordInput: { parse: (data) => create(data, passwordInputSchema) },
OtherInfoInput: { parse: (data) => create(data, otherInfoInputSchema) },
},
initial: {
step: "EmailInput",
context: {}
}
});

funnel.step === 'EmailInput' && typeof funnel.context.email // "string" | "undefined"
funnel.step === 'PasswordInput' && typeof funnel.context.email // "string"

funnel.step === 'PasswordInput' && typeof funnel.context.password // "string" | "undefined"
funnel.step === 'OtherInfoInput' && typeof funnel.context.password // "string"
// ...
}
```
</Tabs.Tab>
</Tabs>

<Callout type="info">
**Runtime Validator Packages?**
These packages validate data and ensure type safety at runtime. They are used for form input validation, API response validation, etc.
</Callout>
Loading

0 comments on commit a91cbaf

Please sign in to comment.