From a91cbaf72b7bb02a2ab8c5297714223ce805769a Mon Sep 17 00:00:00 2001 From: MinuKang Date: Mon, 19 Aug 2024 14:18:41 +0900 Subject: [PATCH] docs: english translation init (#19) * init * fix * fix --- README.md | 6 +- docs/next.config.js | 4 +- docs/src/components/UseFunnelCodeBlock.tsx | 29 ++- docs/src/pages/_meta.en.json | 13 + docs/src/pages/docs/_meta.en.json | 29 +++ docs/src/pages/docs/context-guide.en.mdx | 256 ++++++++++++++++++++ docs/src/pages/docs/context-guide.ko.mdx | 15 +- docs/src/pages/docs/custom-router.en.mdx | 110 +++++++++ docs/src/pages/docs/features.en.mdx | 138 +++++++++++ docs/src/pages/docs/funnel-render.en.mdx | 184 ++++++++++++++ docs/src/pages/docs/funnel-render.ko.mdx | 4 +- docs/src/pages/docs/get-started.en.mdx | 108 +++++++++ docs/src/pages/docs/get-started.ko.mdx | 4 +- docs/src/pages/docs/install.en.mdx | 27 +++ docs/src/pages/docs/install.ko.mdx | 3 +- docs/src/pages/docs/overlay.en.mdx | 204 ++++++++++++++++ docs/src/pages/docs/overview.en.mdx | 19 ++ docs/src/pages/docs/sub-funnel.en.mdx | 122 ++++++++++ docs/src/pages/docs/transition-event.en.mdx | 74 ++++++ docs/src/pages/docs/use-funnel.en.mdx | 153 ++++++++++++ docs/src/pages/docs/use-funnel.ko.mdx | 2 +- docs/src/pages/index.en.mdx | 26 ++ docs/theme.config.tsx | 2 +- 23 files changed, 1506 insertions(+), 26 deletions(-) create mode 100644 docs/src/pages/_meta.en.json create mode 100644 docs/src/pages/docs/_meta.en.json create mode 100644 docs/src/pages/docs/context-guide.en.mdx create mode 100644 docs/src/pages/docs/custom-router.en.mdx create mode 100644 docs/src/pages/docs/features.en.mdx create mode 100644 docs/src/pages/docs/funnel-render.en.mdx create mode 100644 docs/src/pages/docs/get-started.en.mdx create mode 100644 docs/src/pages/docs/install.en.mdx create mode 100644 docs/src/pages/docs/overlay.en.mdx create mode 100644 docs/src/pages/docs/overview.en.mdx create mode 100644 docs/src/pages/docs/sub-funnel.en.mdx create mode 100644 docs/src/pages/docs/transition-event.en.mdx create mode 100644 docs/src/pages/docs/use-funnel.en.mdx create mode 100644 docs/src/pages/index.en.mdx diff --git a/README.md b/README.md index 3bea63c..ae55558 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/next.config.js b/docs/next.config.js index 7f83eaa..a49fd61 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -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', }, }); diff --git a/docs/src/components/UseFunnelCodeBlock.tsx b/docs/src/components/UseFunnelCodeBlock.tsx index 2631678..76de6d4 100644 --- a/docs/src/components/UseFunnelCodeBlock.tsx +++ b/docs/src/components/UseFunnelCodeBlock.tsx @@ -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) { return ( - + p.packageTitle)} storageKey="favorite-package"> {useFunnelPackages.map((item) => ( - - {children} + + {children} ))} @@ -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); } } }); diff --git a/docs/src/pages/_meta.en.json b/docs/src/pages/_meta.en.json new file mode 100644 index 0000000..1c08ad6 --- /dev/null +++ b/docs/src/pages/_meta.en.json @@ -0,0 +1,13 @@ +{ + "index": { + "type": "page", + "display": "hidden", + "theme": { + "layout": "full" + } + }, + "docs": { + "type": "page", + "title": "View Docs" + } +} diff --git a/docs/src/pages/docs/_meta.en.json b/docs/src/pages/docs/_meta.en.json new file mode 100644 index 0000000..f94f12c --- /dev/null +++ b/docs/src/pages/docs/_meta.en.json @@ -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" +} diff --git a/docs/src/pages/docs/context-guide.en.mdx b/docs/src/pages/docs/context-guide.en.mdx new file mode 100644 index 0000000..bfa2910 --- /dev/null +++ b/docs/src/pages/docs/context-guide.en.mdx @@ -0,0 +1,256 @@ +import { Tabs, Callout } from 'nextra/components' +import { UseFunnelCodeBlock, Keyword } from '@/components' + +# Step-by-Step Definition of context + +In `@use-funnel`, you need to define the type of context required at each step (step). Let's look at different ways to define context. + +## Defining with generics + +Define the type of context required for each step as a generic, and pass the object type with step as the key and the type of context as the value to the generics of [`useFunnel()`](./use-funnel.mdx). + + +```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" + // ... +} +``` + + +## Defining with steps object + +You can define step through [`FunnelStepOption`](./use-funnel#funnelstepoption) of [`useFunnel()`](./use-funnel). Define context through `guard()` or `parse()`. + + +```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({ + 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" + // ... +} +``` + + +- `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. + + + If both are defined, `guard()` is executed first. + + +### `createFunnelSteps()` to define + +You can create a step that changes a specific key of the initial context from optional to required when transitioning to the next step simply. + +```tsx +declare function createFunnelSteps(): { + extends: (name: string | string[], options?: { requiredKeys: string | string[] }) => this; + build: () => Record; +} +``` + +- `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`): An object with the step name as the key and the options of each step as the value. + + +```tsx {3-13, 22} +import { createFunnelSteps, useFunnel } from "@use-funnel/next"; + +type FormState = { + email?: string; + password?: string; + other?: unknown; +} + +const steps = createFunnelSteps() + .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" + // ... +} +``` + + +## Using Runtime Validator Packages + +Use runtime validator packages to validate data and ensure type safety. Here are examples using `zod` and `superstruct`. + + + +```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" + // ... +} +``` + + +```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" + // ... +} +``` + + + + +**Runtime Validator Packages?** +These packages validate data and ensure type safety at runtime. They are used for form input validation, API response validation, etc. + \ No newline at end of file diff --git a/docs/src/pages/docs/context-guide.ko.mdx b/docs/src/pages/docs/context-guide.ko.mdx index aa4df26..637b6a0 100644 --- a/docs/src/pages/docs/context-guide.ko.mdx +++ b/docs/src/pages/docs/context-guide.ko.mdx @@ -48,7 +48,7 @@ function MyFunnelApp() { [`useFunnel(){:tsx}`](./use-funnel) 의 [`FunnelStepOption`](./use-funnel#funnelstepoption)을 통해 step 을 정의할 수 있어요. `guard()` 혹은 `parse()` 를 통해 context 를 정의해요. -```tsx {27-31} +```tsx {9-26,35-39} import { useFunnel } from "@use-funnel/next"; type FormState = { @@ -79,15 +79,15 @@ function 그외정보입력_parse (data: unknown): FormState & { password: strin function MyFunnelApp() { const funnel = useFunnel({ id: "step-by-step", + initial: { + step: "이메일입력", + context: {} + }, steps: { 이메일입력: { guard: 이메일입력_guard }, 비밀번호입력: { parse: 비밀번호입력_parse }, - 그외정보입력: { guard: 그외정보입력_parse }, + 그외정보입력: { parse: 그외정보입력_parse }, }, - initial: { - step: "이메일입력", - context: {} - } }) funnel.step === '이메일입력' && typeof funnel.context.email // "string" | "undefined" @@ -128,7 +128,7 @@ declare function createFunnelSteps(): { -```tsx {3-13} +```tsx {3-13, 22} import { createFunnelSteps, useFunnel } from "@use-funnel/next"; type FormState = { @@ -255,6 +255,5 @@ function MyFunnelApp() { **런타임 밸리데이터 패키지(Runtime Validator Packages)란?** - 런타임 밸리데이터 패키지는 런타임에 데이터의 유효성을 검사하는 도구에요. 이 패키지들은 주로 데이터의 형식 검증, 데이터 파싱, 타입 안전성 확보 등을 목적으로 사용해요. 개발자가 정의한 스키마(schema)에 따라 데이터를 검증하고, 오류가 있다면 알려줘요. 이러한 패키지들은 주로 폼 입력 검증, API 응답 검증 등 다양한 상황에서 사용해요. \ No newline at end of file diff --git a/docs/src/pages/docs/custom-router.en.mdx b/docs/src/pages/docs/custom-router.en.mdx new file mode 100644 index 0000000..f5d975e --- /dev/null +++ b/docs/src/pages/docs/custom-router.en.mdx @@ -0,0 +1,110 @@ +# Creating a Custom Router + +`@use-funnel` stores state snapshots of each funnel step in an array, enabling stable backward and forward navigation. Here’s how to create a custom router without relying on the built-in functions provided by router packages. + +First, install the `@use-funnel/core` package + +```shell npm2yarn +npm install @use-funnel/core --save +``` + +Here’s an example of implementing a funnel using only [`useState`](https://react.dev/reference/react/useState), without a router. + +```tsx filename="src/hooks/useFunnel.ts" +import { useMemo, useState } from 'react'; +import { createUseFunnel } from '@use-funnel/core'; + +// Define the useFunnel hook using the createUseFunnel function. +export const useFunnel = createUseFunnel(({ id, initialState }) => { + // history manages the array of state snapshots for the funnel. + const [history, setHistory] = useState(() => [initialState]); + // currentIndex manages the index of the current funnel step. + const [currentIndex, setCurrentIndex] = useState(0); + + return useMemo( + () => ({ + // Returns the history and currentIndex, representing the current state of the funnel. + history, + currentIndex, + // push function adds a new state and updates the current index. + push(state) { + setHistory((prev) => { + const next = prev.slice(0, currentIndex + 1); + return [...next, state]; + }); + setCurrentIndex((prev) => prev + 1); + }, + // replace function replaces the current state and updates the state snapshot. + replace(state) { + setHistory((prev) => { + const next = prev.slice(0, currentIndex); + return [...next, state]; + }); + }, + // go function moves the current index by delta. + go(delta) { + setCurrentIndex((prev) => prev + delta); + }, + }), + [history, currentIndex] // Returns memoized values whenever history and currentIndex change. + ); +}); +``` + +The above code example uses `useState` to manage the funnel’s state. The `createUseFunnel` function creates a hook that manages the array of state snapshots (`history`) and the current index (`currentIndex`). + +- `push(state)`: Adds a new state and updates the current index. +- `replace(state)`: Overwrites the current state with a new state and updates the state snapshot. +- `go(delta)`: Moves the current index by `delta`. + +Next, let's look at the `CreateUseFunnelOptions` interface for the options object passed to `createUseFunnel()`. + +```tsx +interface CreateUseFunnelOptions { + /** + * A unique identifier for the funnel. This distinguishes global state when there are multiple funnels on one page. + */ + id: string; + /** + * The initial state of the funnel passed as `initial` in `useFunnel()`. This state is used at the entry of the funnel. + */ + initialState: FunnelState; +} + +/** + * An interface representing the state of the funnel. `step` indicates the name of the funnel step, and `context` represents the state required for that step. + */ +interface FunnelState { + step: string; + context: T; +} +``` + +Finally, let’s examine the `CreateUseFunnelResult` interface, which that describes the result returned by `createUseFunnel()`. This interface includes methods for managing funnel state and transitions. + +```tsx +interface CreateUseFunnelResult { + /** + * An array of state snapshots of the funnel. It holds the routing history accumulated after entering the funnel. + */ + history: FunnelState[]; + /** + * The index of the current funnel step in progress. + */ + currentIndex: number; + /** + * Implements the action of adding a new state to the current `history` and moving to the next funnel step. + */ + push(state: FunnelState): void | Promise; + /** + * Implements the action of overwriting the current `history` with a new state and moving to the next funnel step. + */ + replace(state: FunnelState): void | Promise; + /** + * Implements the action of moving by `delta` in the `history` array. + */ + go(delta: number): void or Promise; +} +``` + +This interface includes various methods to manage the funnel state and handle step transitions, ensuring robust state management for your funnel. diff --git a/docs/src/pages/docs/features.en.mdx b/docs/src/pages/docs/features.en.mdx new file mode 100644 index 0000000..dc22183 --- /dev/null +++ b/docs/src/pages/docs/features.en.mdx @@ -0,0 +1,138 @@ +import { Keyword, KeywordDescription } from '@/components'; + +# Main Features + +## Overview + +`@use-funnel` helps safely manage the state of each step safely with strong type support and ensures consistent history state, ensuring the user has the correct state at each step. + +## Managing UI State with Strong Type Safety + +When developing a UI composed of multiple steps, it's important to manage the state of each step. `@use-funnel` allows you to define and manage the state of each step with type safety. + +### Issues Implementing Complex UI Flows Without `@use-funnel` + +Without `@use-funnel`, managing the state of a multi-step UI can become complex. + +```tsx +type Funnel = + | { step: "A"; context: { a: string; b?: string; c?: string; } } + | { step: "B"; context: { a: string; b: string; c?: string; } } + | { step: "C"; context: { a: string; b: string; c: string; } }; + +const [funnel, setFunnel] = useState({ step: "A", context: { a: "a" } }); +``` + +However, this approach has several issues: + +```tsx +// When transitioning from A to B, only `b` should be required, but `a` is also required. +setFunnel({ step: "B", context: { b: "new b" } }) // compile error +setFunnel({ step: "B", context: { a: funnel.context.a, b: "new b" } }) // no compile error +``` + +- **Complex History Management**: You need to implement history operations like going back and forward separately. +- **Difficulty in State Transitions**: It's can be difficult to isolate only the required states when transitioning between steps. For example, when transitioning from step A to B, only `b` should be required, but in a typical implementation, `a` might also be required. + +## Easy Management with `@use-funnel` + +The following code demonstrates defining two steps, A and B, using `useFunnel`. + +- Each step has its unique context. +- The initial step is A, where the type of `context.a` is `string` or `undefined`. +- When the step changes to B, the type of `context.a` is fixed as `string`. + +```tsx +const funnel = useFunnel<{ + A: { a?: string; b?: string }; + B: { a: string; b?: string }; +}>({ + id: "strongly-typed", + initial: { + step: "A", + context: {} + } +}); + +// When the initial step is "A", the type of context.a is "string" or "undefined" +funnel.step === "A" && typeof funnel.context.a // "string" | "undefined" +// When the step changes to "B", the type of context.a is "string" +funnel.step === "B" && typeof funnel.context.a // "string" +``` + +By ensuring type safety for the context property of each step guarantees that you can clearly define the necessary context and prevent errors. + +--- + +Next is an example of code that throws an error because it doesn't match the defined type. + +```tsx {3,15} +const funnel = useFunnel<{ + A: { a?: string; b?: string; c?: string }; + B: { a: string; b: string; c?: string }; + C: { a: string; b: string; c: string }; +}>({ + id: "strongly-typed", + initial: { + step: "A", + context: { a: "a" } + } +}); + +if (funnel.step === "A") { + funnel.history.push("B", {}); + // ^ '{}' is not assignable to type '{ a: string; b: string; c?: string; }'. +} +``` + +{/* 이 코드에서 에러가 발생하는 이유는 빈 객체 `{}`를 사용해서 `B` step으로 전환하려고 시도하기 때문이에요. B step의 타입 정의에 따르면, `b`는 필수로 값이 필요한 프로퍼티에요. 빈 객체는 이러한 타입 정의와 다르기 때문에 에러가 발생합니다. + */} +The error occurs because you are trying to transition to the `B` step using an empty object `{}`. According to the type definition of the `B` step, `b` is a required property. Since an empty object does not match this type definition, an error occurs. + +To resolve the error, you need to provide the required property `b` value when transitioning to the `B` step. For example, you can modify the code as follows: + +```tsx +if (funnel.step === "A") { + funnel.history.push("B", { b: "required value" }); +} +``` + +Now that the type definition is satisfied, the error no longer occurs. + +## Easy History State Management + +Effective history state management is crucial for implementing complex UI flows. `@use-funnel` manages state and history together, storing state snapshots and updating the current state based on route changes. + +### History Management + +With `@use-funnel`, you can easily manage the history of state transitions. This ensures that navigating back and forth between steps maintains the correct state. + +Consider a simple funnel with three steps: A → B → C. + +```mermaid +sequenceDiagram + A->>B: state: { A: true } + B->>C: state: { A: true, B: false } +``` + +After selecting `true` in step A and `false` in step B, you move to step C. If the user navigates back from step C, what should the state be? + +```mermaid +sequenceDiagram + A->>B: state: { A: true } + B->>C: state: { A: true, B: false } + C->>B: ? +``` + +Ideally, you would expect the state to return to the state selected in step B: `state: { A: true, B: false }`. However, if the state is separated from the history, navigating back may not restore the state correctly, and only the latest state may be maintained. + +```mermaid +sequenceDiagram + A->>B: state: { A: true } + B->>C: state: { A: true, B: false } + C->>B: state: { A: true } +``` + +`@use-funnel` solves this problem by managing history and state together. Each time the funnel transitions to the next step, it saves a state snapshot in the history. When the route changes, the current state is updated accordingly. + +If necessary, you can customize the router to update the state directly in local storage or on the server. For more information, see the [Custom Router](/docs/custom-router) documentation. \ No newline at end of file diff --git a/docs/src/pages/docs/funnel-render.en.mdx b/docs/src/pages/docs/funnel-render.en.mdx new file mode 100644 index 0000000..d5e2f4f --- /dev/null +++ b/docs/src/pages/docs/funnel-render.en.mdx @@ -0,0 +1,184 @@ +import { Callout } from 'nextra/components' +import { Keyword, UseFunnelCodeBlock } from '@/components' + +# `funnel.Render` Component + +The `funnel.Render` component is responsible for rendering each step of the funnel. + +It is included in the [UseFunnelResults](/docs/use-funnel#usefunnelresults) returned by [`useFunnel()`](./use-funnel). + +```tsx +interface FunnelRenderComponent extends React.ComponentType> { + with: FunnelRenderWithEvent; + overlay: FunnelRenderOverlay; +}; +``` + +- [FunnelRenderProps](#funnelrenderprops) +- [FunnelRenderWithEvent](#funnelrenderwithevent) +- [FunnelRenderOverlay](#funnelrenderoverlay) + +## FunnelRenderProps + +Specifies the rendering logic for each step. + +```typescript +interface FunnelRenderProps { + [key in keyof T]: (props: { + context: T[key]; + history: FunnelHistory; + }) => React.ReactNode; +} +``` + +- `context` (`object`): An object representing the state of the current step. +- `history` (`object`): An object managing the transitions of the funnel. Use it to move to the next step or return to the previous step. For more information, see [`FunnelHistory`](./use-funnel#funnelhistory). + +### Example + +In the following example demonstrates how to define the rendering logic for each step (`EmailInput`, `PasswordInput`, `OtherInput`) and pass the necessary data for each step. You can access the data for each step using `context` and move to the next step using `history`. + + +```tsx +import { useFunnel } from "@use-funnel/next"; + +const funnel = useFunnel(/* ... */); +return ( + ( + history.push('PasswordInput', { email })} /> + )} + PasswordInput={({ context, history }) => ( + history.push('OtherInput', { password })} + /> + )} + OtherInput={() => } + /> +); +``` + + +## FunnelRenderWithEvent + +Enables the definition of an event object for multiple transitions within the current step. + +```typescript +interface FunnelRenderWithEvent { + events: { + [eventName: string]: (payload: any, funnel: { context: T; history: FunnelHistory }) => void; + }; + render: (props: { + context: T; + dispatch: (eventName: string, payload: any) => void; + }) => React.ReactNode; +} +``` + +- `events` (`object`): An object containing event handlers for each event name. + - `eventName` (`string`): The name of the event. + - `payload` (`any`): The data passed to the event handler. + - `funnel` (`object`): An object containing the current step state and the funnel history. Use it to access the current state and manage transitions. + - `context` (`object`): An object representing the state of the current step. + - `history` (`object`): An object managing the transitions of the funnel. Use it to move to the next step or return to the previous step. For more information, see [`FunnelHistory`](./use-funnel#funnelhistory). +- `render` (`function`): A function that returns a React node. + - `context` (`object`): An object representing the state of the current step. + - `dispatch` (`function`): A function that dispatches an event. + - `eventName` (`string`): The name of the event. + - `payload` (`any`): The data passed to the event handler. + +### Example + +```tsx + history.push('PasswordInput', { email }), + EmailInputFail: (error: Error, { history }) => history.push('ErrorStep', { error: error.message }) + }, + render({ context, dispatch }) { + return ( + dispatch('EmailInputSuccess', email)} + onError={(error) => dispatch('EmailInputFail', error)} + /> + ); + } + })} +/> +``` + +For more detailed instructions, see the [Define transition event](https://use-funnel.slash.page/docs/transition-event). + +## FunnelRenderOverlay + +This option allows you to display the previous step on the same screen when displaying the current step, you can use this option. + +```typescript +type FunnelRenderOverlay = + | FunnelRenderOverlayWithoutEvent + | FunnelRenderOverlayWithEvent; + +interface FunnelRenderOverlayWithoutEvent { + render: (props: { + context: T; + history: FunnelHistory; + close: () => void; + }) => React.ReactNode; +} + +interface FunnelRenderOverlayWithEvent { + events: { + [eventName: string]: (payload: any, funnel: { context: T; history: FunnelHistory }) => void; + }; + render: (props: { + context: T; + dispatch: (eventName: string, payload: any) => void; + close: () => void; + }) => React.ReactNode; +} +``` + +#### FunnelRenderOverlayWithoutEvent + +- `render` (`function`): A function that returns a React node. + - `context` (`object`): An object representing the state of the current step. + - `history` (`object`): An object managing the transitions of the funnel. Use it to move to the next step or return to the previous step. For more information, see [`FunnelHistory`](./use-funnel#funnelhistory). + - `close` (`function`): A function that closes the overlay. + +#### FunnelRenderOverlayWithEvent + +- `events` (`object`): An object containing event handlers for each event name. + - `eventName` (`string`): The name of the event. + - `payload` (`any`): The data passed to the event handler. + - `funnel` (`object`): An object containing the current step state and the funnel history. Use it to access the current state and manage transitions. + - `context` (`object`): An object representing the state of the current step. + - `history` (`object`): An object managing the transitions of the funnel. Use it to move to the next step or return to the previous step. For more information, see [`FunnelHistory`](./use-funnel#funnelhistory). +- `render` (`function`): A function that returns a React node. + - `context` (`object`): An object representing the state of the current step. + - `dispatch` (`function`): A function that dispatches an event. + - `eventName` (`string`): The name of the event. + - `payload` (`any`): The data passed to the event handler. + - `close` (`function`): A function that closes the overlay. + +### Example + +```tsx + history.push('PasswordInput', { email })} + onClose={() => close()} + /> + ); + } + })} +/> +``` + +For more detailed instructions, see the [Use overlay](https://use-funnel.slash.page/docs/overlay). \ No newline at end of file diff --git a/docs/src/pages/docs/funnel-render.ko.mdx b/docs/src/pages/docs/funnel-render.ko.mdx index a7231c8..bca0eca 100644 --- a/docs/src/pages/docs/funnel-render.ko.mdx +++ b/docs/src/pages/docs/funnel-render.ko.mdx @@ -5,7 +5,7 @@ import { Keyword, UseFunnelCodeBlock } from '@/components' `funnel.Render />`는 퍼널의 각 step를 렌더링해요. -[`useFunnel(){:tsx}`](./use-funnel.ko.mdx)의 반환값인 [UseFunnelResults](/docs/use-funnel#usefunnelresults)에 포함되어 있어요. +[`useFunnel(){:tsx}`](./use-funnel)의 반환값인 [UseFunnelResults](/docs/use-funnel#usefunnelresults)에 포함되어 있어요. ```tsx interface FunnelRenderComponent extends React.ComponentType> { @@ -32,7 +32,7 @@ interface FunnelRenderProps { ``` - `context` (`object`): 현재 step의 상태를 나타내는 객체에요. 현재 step에서 필요한 데이터에 접근할 수 있어요. -- `history` (`object`): 퍼널의 이동을 관리하는 객체에요. 다음 step로 이동하거나 이전 step로 돌아갈 때 사용해요. 자세한 내용은 [`FunnelHistory`](./use-funnel.ko.mdx#funnelhistory)를 참고하세요. +- `history` (`object`): 퍼널의 이동을 관리하는 객체에요. 다음 step로 이동하거나 이전 step로 돌아갈 때 사용해요. 자세한 내용은 [`FunnelHistory`](./use-funnel#funnelhistory)를 참고하세요. ### 예시 diff --git a/docs/src/pages/docs/get-started.en.mdx b/docs/src/pages/docs/get-started.en.mdx new file mode 100644 index 0000000..d168aa9 --- /dev/null +++ b/docs/src/pages/docs/get-started.en.mdx @@ -0,0 +1,108 @@ +import { Steps, Callout } from 'nextra/components' +import { UseFunnelCodeBlock, Keyword } from '@/components' + +# Getting Started + +Let's implement a simple sign-up flow using `@use-funnel`. You'll learn how to define and manage the state of each step safely and step by step. + + +### Defining context step by step + +The sign-up process can be divided into several steps. Here, we'll divide it into three steps: email input, password input, and other information input. And we'll define the state required for each step as follows. + +```tsx filename="context.ts" +// 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 } +``` + +- `EmailInput`: The first step of sign-up. Although there are email and password input fields, nothing has been entered yet. Both `email` and `password` are defined as optional fields. + +- `PasswordInput`: The second step of sign-up. After the user enters the email, they enter the password. In this step, the `email` field must be entered, and the `password` is an optional field. + +- `OtherInfoInput`: The third step of sign-up. After the user enters both the email and password, they enter additional information. In this step, both `email` and `password` must be entered. + +By defining the state of each step as a type, you can maintain [type safety](./features.mdx#ui-state-managed-with-strong-type-safety) in your code and easily track the information required for each step. + +### Configuring the initial step + +Now let's set the initial step using [`useFunnel()`](./use-funnel). + +First, specify the object type with the key as the step and the context object as the value in the generics of [`useFunnel()`](./use-funnel). Pass the type you defined for each step in the previous step to [`useFunnel()`](./use-funnel). Set the inital step and context object to be used when entering the component in the `initial` property. + +Here, we set the initial step to "EmailInput" and an empty `context` object to be used in that step. `id` is a unique identifier to distinguish when there are multiple funnels in one component. + + +```tsx {5-15} +import { useFunnel } from "@use-funnel/next"; +import type { EmailInput, PasswordInput, OtherInfoInput } "./context"; + +function MyFunnelApp() { + const funnel = useFunnel<{ + EmailInput: EmailInput; + PasswordInput: PasswordInput; + OtherInfoInput: OtherInfoInput; + }>({ + id: "my-funnel-app", + initial: { + step: "EmailInput", + context: {} + } + }); + // ... +} +``` + + +For other ways to define the state of each step, see the [state definition guide](/docs/context-guide). + +### Using context and history step by step + +[`useFunnel()`](./use-funnel) returns `context` and `history` based on the `step`. You can configure the UI for each step and manage the required state and events. + +```tsx +declare function EmailInput(props: { onNext: (email: string) => void }): JSX.Element; +declare function PasswordInput(props: { onNext: (password: string) => void }): JSX.Element; +declare function OtherInfoInput(props: { onNext: (other: unknown) => void }): JSX.Element; + +switch (funnel.step) { + case "EmailInput": + return funnel.history.push("PasswordInput", { email })} />; + case "PasswordInput": + return funnel.history.push("OtherInfoInput", { ...funnel.context, password })} />; + case "OtherInfoInput": + return funnel.history.push("Finish", { ...funnel.context, other })} />; +} +``` + +- `funnel.context{:jsx}`: You can get the `context` of the current `step`. For example, in the "EmailInput" step, `funnel.context.email` is of type `string | undefined`, but in the "PasswordInput" step, it can be inferred as type `string`. +- `funnel.history.push(){:jsx}`: You can move to the next step. Pass the desired `step` as the first argument to `push()` and the necessary `context` for that `step` as the second argument. +- `funnel.history.replace(){:jsx}`: It's basic behavior is similar to `funnel.history.push()`, but it replaces the current `step` without adding to the history stack. + +#### Note: Implementing easily using `` + +To centralize the rendering logic for each step, you can use ``. This component allows you to define the rendering logic for each step and pass the necessary data for each step. + +```tsx +return ( + ( + history.push("PasswordInput", { email })} /> + )} + PasswordInput={({ context, history }) => ( + history.push("OtherInfoInput", { ...context, password })} /> + )} + OtherInfoInput={({ context, history }) => ( + history.push("Finish", { ...context, other })} /> + )} + Finish={() => } + /> +); +``` + +The detailed usage of the [``](./funnel-render) component is available in the [reference](/docs/funnel-render). + + \ No newline at end of file diff --git a/docs/src/pages/docs/get-started.ko.mdx b/docs/src/pages/docs/get-started.ko.mdx index 8e53584..d7b88f4 100644 --- a/docs/src/pages/docs/get-started.ko.mdx +++ b/docs/src/pages/docs/get-started.ko.mdx @@ -61,7 +61,7 @@ function MyFunnelApp() { ### 단계별 contexthistory 사용하기 -[`useFunnel(){:tsx}`](./use-funnel.ko.mdx)에서 반환된 `step`에 따라 `context`와 `history`를 사용해요. 각 단계별로 UI를 구성하고, 필요한 상태와 이벤트를 처리할 수 있어요. +[`useFunnel(){:tsx}`](./use-funnel)에서 반환된 `step`에 따라 `context`와 `history`를 사용해요. 각 단계별로 UI를 구성하고, 필요한 상태와 이벤트를 처리할 수 있어요. ```tsx declare function 이메일입력(props: { onNext: (email: string) => void }): JSX.Element; @@ -109,6 +109,6 @@ return ( ) ``` -[`{:jsx}`](./funnel-render.ko.mdx) 컴포넌트를 사용하는 자세한 방법은 [레퍼런스](/docs/funnel-render)를 참고해주세요. +[`{:jsx}`](./funnel-render) 컴포넌트를 사용하는 자세한 방법은 [레퍼런스](/docs/funnel-render)를 참고해주세요. \ No newline at end of file diff --git a/docs/src/pages/docs/install.en.mdx b/docs/src/pages/docs/install.en.mdx new file mode 100644 index 0000000..91756c0 --- /dev/null +++ b/docs/src/pages/docs/install.en.mdx @@ -0,0 +1,27 @@ +# Installation + +`@use-funnel` is compatible with various routers. including: + +- [react-router-dom](https://reactrouter.com/web/guides/quick-start) +- [next.js page router](https://nextjs.org/docs/pages) + - For the [next.js app router](https://nextjs.org/docs/app-building), use [browser history](https://developer.mozilla.org/en-US/docs/Web/API/History). +- [@react-navigation/native](https://reactnavigation.org/docs/getting-started) +- [browser history](https://developer.mozilla.org/en-US/docs/Web/API/History) + +## Supported React Version + +| React | ^16.8 | +| :---: | :---: | + +## Installation + +Select the router you want to use and install the corresponding package with the following command: + +```shell npm2yarn +npm install @use-funnel/react-router-dom --save +npm install @use-funnel/next --save +npm install @use-funnel/react-navigation-native --save +npm install @use-funnel/browser --save +``` + +If the router you need isn't listed or if you prefer to manage history directly, refer to [Creating a Custom Router](/docs/custom-router). \ No newline at end of file diff --git a/docs/src/pages/docs/install.ko.mdx b/docs/src/pages/docs/install.ko.mdx index 30241da..452796a 100644 --- a/docs/src/pages/docs/install.ko.mdx +++ b/docs/src/pages/docs/install.ko.mdx @@ -3,7 +3,8 @@ `@use-funnel`은 다양한 라우터와 호환됩니다. 다음 라우터를 지원하고 있어요. - [react-router-dom](https://reactrouter.com) -- [next.js](https://nextjs.org) +- [next.js page router](https://nextjs.org/docs/pages) + - [next.js app router](https://nextjs.org/docs/app/building-your-application) 는 [browser history](https://developer.mozilla.org/en-US/docs/Web/API/History)를 사용해주세요. - [@react-navigation/native](https://reactnavigation.org/) - [browser history](https://developer.mozilla.org/en-US/docs/Web/API/History) diff --git a/docs/src/pages/docs/overlay.en.mdx b/docs/src/pages/docs/overlay.en.mdx new file mode 100644 index 0000000..1cfcc1a --- /dev/null +++ b/docs/src/pages/docs/overlay.en.mdx @@ -0,0 +1,204 @@ +import { Callout } from 'nextra/components' +import { Sandpack } from '@/components' + +# Using Overlay + +
+![Overlay example](/overlay.png) +
+ +The overlay is used to display UI such as modals, bottom sheets, and similar components. while maintaining the previous step as shown in the image on the right. + +In the following example, when moving from the "School Input" step to the "Start Date Input" step, an overlay is used. When the `` component is rendered, the UI of the previous step `` is remains visible. + +```tsx {7} + ( + history.push('StartDateInput', { school })} + /> + )} + StartDateInput={funnel.Render.overlay({ + render({ history, close }) { + return ( + history.push('NextStep', { startDate })} + onClose={() => close()} + /> + ); + } + })} +/> +``` + +
+ + +**Note:** If the overlay is closed by an interaction other than the router's back button, you need to explicitly execute `close()` in the `render()` argument. Executing `close()` will navigate to the previous step through the history. + + +## Example + + + +```tsx Example.tsx active +import { useFunnel } from "@use-funnel/react-router-dom"; +import { OverlayProvider } from "overlay-kit"; + +import { SchoolInput } from "./SchoolInput"; +import { StartDateInputBottomSheet } from "./StartDateInputBottomSheet"; + +export const Example = () => { + const funnel = useFunnel<{ + SchoolInput: { school?: string }; + StartDateInput: { school: string; startDate?: string }; + NextStep: { school: string; startDate: string }; + }>({ + id: "overlay-example", + initial: { + step: "SchoolInput", + context: {}, + }, + }); + + return ( + + ( + history.push('StartDateInput', { school })} + /> + )} + StartDateInput={funnel.Render.overlay({ + render({ history, close }) { + return ( + history.push('NextStep', { startDate })} + onClose={() => close()} + /> + ); + } + })} + /> + + ); +}; +``` + + +```tsx SchoolInput.tsx +import { useState } from "react"; +import { RadioGroup, Button, Heading } from "@radix-ui/themes"; + +interface Props { + onNext: (school: string) => void; +} + +export function SchoolInput({ onNext }: Props) { + const [school, setSchool] = useState("A") + return ( +
+ Select your school + + A School + B School + C School + + +
+ ) +}; +``` + +```tsx StartDateInputBottomSheet.tsx +import { useEffect, useRef } from "react"; +import { overlay } from "overlay-kit"; +import { Dialog, Flex, TextField, Text, Button } from "@radix-ui/themes"; + +interface Props { + onNext: (startDate: string) => void; + onClose: () => void; +} + +export function StartDateInputBottomSheet({ onNext, onClose }: Props) { + const inputRef = useRef(null); + useEffect(() => { + const overlayId = overlay.open(({ isOpen, close }) => ( + { + onClose() + close() + }}> + + Enter your start date + + + + + + + + + + + + )); + return () => { + overlay.unmount(overlayId); + } + }, []); + return null; +}; + +``` + +
+ + +**Note:** The `overlay-kit` library is used in the example above. You can use any overlay library you prefer. + + +## Overlay with event example + +To define an overlay along with [transition events](/docs/transition-event), use the `events` property. + +In the following example, the "Email Input" step uses transition events and an overlay together. Define the "Email Input Complete" and "Email Input Fail" events, and trigger the transition to the next step for each event. + +```tsx + history.push('PasswordInput', { email }), + EmailInputFail: (error: Error, { history }) => history.push('ErrorPage', { error: error.message }) + }, + render({ context, dispatch }) { + return ( + dispatch('EmailInputComplete', email)} + onError={(error) => dispatch('EmailInputFail', error)} + /> + ); + } + })} +/> +``` + + +You cannot use `history` in the rendering function when defining events. Instead, you should trigger the events defined in the `events` object using `dispatch()`. + \ No newline at end of file diff --git a/docs/src/pages/docs/overview.en.mdx b/docs/src/pages/docs/overview.en.mdx new file mode 100644 index 0000000..b6257f3 --- /dev/null +++ b/docs/src/pages/docs/overview.en.mdx @@ -0,0 +1,19 @@ +import { Keyword, KeywordDescription } from '@/components'; + +# Overview + +