Skip to content

Commit

Permalink
Merge pull request #1 from XionWCFM/feature
Browse files Browse the repository at this point in the history
[2024.08.07] Funnel Core Interface Breaking Change
  • Loading branch information
XionWCFM authored Aug 6, 2024
2 parents 2e2318b + 052fb1f commit 1a12c4e
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 119 deletions.
19 changes: 4 additions & 15 deletions apps/app-router-example/app/funnel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
"use client";

import { useFunnel } from "@xionhub/funnel-app-router-adapter";
import { funnelOptions } from "@xionhub/funnel-core";
import { overlay } from "overlay-kit";
import { useEffect } from "react";

const EXAMPLE_FUNNEL_ID = "hello-this-is-funnel-id";
const exampleFunnelOptions = funnelOptions({
steps: ["a", "b", "c"],
funnelId: EXAMPLE_FUNNEL_ID,
});
import { exampleFunnelOptions } from "~/src/example-funnel";

export default function ExampleFunnel() {
const [Funnel, controller] = useFunnel(exampleFunnelOptions);

// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
controller.onStepChange("a");
}, []);
const [Funnel, controller] = useFunnel(exampleFunnelOptions());

return (
<div className=" px-4 py-4">
Expand All @@ -35,14 +23,15 @@ export default function ExampleFunnel() {
await new Promise((res) => setTimeout(res, 1000));
return false;
}}
onFunnelRestrictEvent={async () => {
onRestrict={async () => {
await overlay.openAsync(({ close, unmount }) => (
<div>
<div>접근할 수 없는 상태에요</div>
<button
type="button"
onClick={() => {
close(true);
unmount();
}}
>
처음 화면으로 돌아가기
Expand Down
12 changes: 12 additions & 0 deletions apps/app-router-example/app/funnel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Suspense } from "react";
import ExampleFunnel from "../funnel";

export default function Page() {
return (
<div>
<Suspense>
<ExampleFunnel />
</Suspense>
</div>
);
}
11 changes: 6 additions & 5 deletions apps/app-router-example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Suspense } from "react";
import ExampleFunnel from "./funnel";
"use client";
import { FunnelClient } from "@xionhub/funnel-core";
import Link from "next/link";
import { exampleFunnelOptions } from "~/src/example-funnel";

export default function Home() {
const funnelClient = new FunnelClient(exampleFunnelOptions());
return (
<div>
<Suspense>
<ExampleFunnel />
</Suspense>
<Link href={`/funnel${funnelClient.createStep("a")}`}>퍼널로 이동하기</Link>
</div>
);
}
8 changes: 8 additions & 0 deletions apps/app-router-example/src/example-funnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FunnelClient, funnelOptions } from "@xionhub/funnel-core";

const EXAMPLE_FUNNEL_ID = "hello-this-is-funnel-id";
export const exampleFunnelOptions = () =>
funnelOptions({
steps: ["a", "b", "c"] as const,
funnelId: EXAMPLE_FUNNEL_ID,
});
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
import {
DEFAULT_FUNNEL_STEP_ID,
type FunnelAdapterReturnType,
type CreateFunnelStepFunction,
FunnelClient,
type FunnelOptions,
type FunnelStepChangeFunction,
type NonEmptyArray,
funnelQs,
useCoreFunnel,
} from "@xionhub/funnel-core";
import { useRouter, useSearchParams } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

export const useFunnelAppRouterAdapter = <Steps extends NonEmptyArray<string>>(
options: Omit<FunnelOptions<Steps>, "step">,
): FunnelAdapterReturnType<Steps> => {
const funnelId = options?.funnelId ?? DEFAULT_FUNNEL_STEP_ID;
) => {
const funnelId = options.funnelId;
const router = useRouter();

const queryStep = useSearchParams().get(funnelId);
const searchParams = useSearchParams();
const queryStep = searchParams.get(funnelId);
const funnelClient = new FunnelClient(options);
const pathname = usePathname();
const step = (queryStep ?? undefined) as Steps[number] | undefined;
const [Funnel, controller] = useCoreFunnel({ ...options, step });

const onStepChange: FunnelAdapterReturnType<Steps>["1"]["onStepChange"] = (newStep, options) => {
const createFunnelStep: CreateFunnelStepFunction<Steps> = (step, options) => {
if (!options?.searchParams) {
return { [funnelId]: step };
}
const allQueryString = funnelClient.getQueryString(options?.searchParams);

const deleteKeyList = Array.isArray(options?.deleteQueryParams)
? options?.deleteQueryParams
: ([options?.deleteQueryParams].filter(Boolean) as string[]);

const value = funnelQs.updateFunnelQs({ [funnelId]: newStep }, deleteKeyList);
const stepObject = funnelClient.createStepObject(step, funnelClient.deleteStep(allQueryString, deleteKeyList));
return stepObject;
};

const newUrl = `${funnelQs.getPathName()}${value}`;
const onStepChange: FunnelStepChangeFunction<Steps> = (newStep, options) => {
const stepObject = createFunnelStep(newStep, { ...options, searchParams });
const newUrl = `${pathname}${funnelClient.stringifyStep(stepObject)}`;

if (options?.type === "replace") {
return router.replace(newUrl);
Expand All @@ -38,5 +49,5 @@ export const useFunnelAppRouterAdapter = <Steps extends NonEmptyArray<string>>(
return router.push(newUrl);
};

return [Funnel, { ...controller, onStepChange }] as const;
return [Funnel, { ...controller, onStepChange, createFunnelStep }] as const;
};
1 change: 0 additions & 1 deletion packages/funnel-core/src/external/constant.ts

This file was deleted.

40 changes: 35 additions & 5 deletions packages/funnel-core/src/external/external-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULT_FUNNEL_STEP_ID } from "./constant";
import qs from "qs";
import type { FunnelOptions, NonEmptyArray } from "./types";

export const funnelOptions = <T extends NonEmptyArray<string>>(props: FunnelOptions<T>): FunnelOptions<T> => props;
Expand All @@ -8,12 +8,42 @@ export class FunnelClient<T extends NonEmptyArray<string>> {
steps: T;

constructor(props: FunnelOptions<T>) {
this.funnelId = props.funnelId ?? DEFAULT_FUNNEL_STEP_ID;
this.funnelId = props.funnelId;
this.steps = props.steps;
}

createFunnelStep(value: T[number]) {
const funnelId = this.funnelId ?? DEFAULT_FUNNEL_STEP_ID;
return `${funnelId}=${value}` as const;
createStep(value: T[number], context?: Record<string, unknown>) {
return this.stringifyStep(this.createStepObject(value, context));
}

getQueryString<T extends Record<string, unknown>>(searchParams: URLSearchParams) {
const result = {} as Record<string, unknown>;
searchParams.forEach((value, key) => {
result[key] = value;
});
return result as T;
}

createStepObject(value: T[number], context?: Record<string, unknown>) {
return { ...context, [this.funnelId]: value } as const;
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
deleteStep<T extends Record<string, any>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const result = { ...obj };

for (const key of keys) {
delete result[key];
}

return result as Omit<T, K>;
}

stringifyStep(context: Record<string, unknown>) {
return qs.stringify(context, { addQueryPrefix: true });
}

parseQueryString<T>(queryString: string) {
return qs.parse(queryString, { ignoreQueryPrefix: true }) as T;
}
}
8 changes: 4 additions & 4 deletions packages/funnel-core/src/external/guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from "react";
import type { GuardProps } from "./types";

export const Guard = ({ condition, children, fallback, onFunnelRestrictEvent }: GuardProps) => {
export const Guard = ({ condition, children, fallback, onRestrict }: GuardProps) => {
const [isRender, setIsRender] = useState(false);
const isOnce = useRef(true);
const canImmediateRender =
Expand All @@ -19,15 +19,15 @@ export const Guard = ({ condition, children, fallback, onFunnelRestrictEvent }:
if (typeof condition === "function") {
result = await condition();
if (result === false) {
onFunnelRestrictEvent?.();
onRestrict?.();
} else {
setIsRender(true);
}
}

if (typeof condition === "boolean") {
if (condition === false) {
onFunnelRestrictEvent?.();
onRestrict?.();
} else {
setIsRender(true);
}
Expand All @@ -38,7 +38,7 @@ export const Guard = ({ condition, children, fallback, onFunnelRestrictEvent }:
check();
isOnce.current = false;
}
}, [canImmediateRender, condition, onFunnelRestrictEvent]);
}, [canImmediateRender, condition, onRestrict]);

return canImmediateRender || isRender ? children : fallback;
};
63 changes: 0 additions & 63 deletions packages/funnel-core/src/external/query-string.ts

This file was deleted.

14 changes: 11 additions & 3 deletions packages/funnel-core/src/external/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export type NonEmptyArray<T> = readonly [T, ...T[]];

export type RoutesEventType = "replace" | "push" | "back";

export type DeleteQueryParams = { deleteQueryParams?: string[] | string };

export type FunnelStepChangeFunction<T extends NonEmptyArray<string>> = (
step: T[number],
options?: { type?: RoutesEventType; deleteQueryParams?: string[] | string },
options?: { type?: RoutesEventType } & DeleteQueryParams,
) => void;

export type RouteFunnelProps<Steps extends NonEmptyArray<string>> = Omit<FunnelProps<Steps>, "steps" | "step">;
Expand All @@ -25,10 +27,15 @@ export interface StepProps<Steps extends NonEmptyArray<string>> {
export interface GuardProps {
condition: boolean | (() => boolean | Promise<boolean>);
children?: ReactNode;
onFunnelRestrictEvent?: () => void;
onRestrict?: () => void;
fallback?: ReactNode;
}

export type CreateFunnelStepFunction<Steps extends NonEmptyArray<string>> = (
step: Steps[number],
options?: { deleteQueryParams?: string[] | string; searchParams?: URLSearchParams },
) => Record<string, unknown>;

export type FunnelAdapterReturnType<Steps extends NonEmptyArray<string>> = [
((props: RouteFunnelProps<Steps>) => JSX.Element) & {
Step: (props: StepProps<Steps>) => JSX.Element;
Expand All @@ -38,11 +45,12 @@ export type FunnelAdapterReturnType<Steps extends NonEmptyArray<string>> = [
funnelId: string;
step: Steps[number] | undefined;
onStepChange: FunnelStepChangeFunction<Steps>;
createFunnelStep: CreateFunnelStepFunction<Steps>;
},
];

export type FunnelOptions<T extends NonEmptyArray<string>> = {
steps: T;
step?: T[number] | undefined;
funnelId?: string;
funnelId: string;
};
26 changes: 18 additions & 8 deletions packages/funnel-core/src/external/use-core-funnel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { useMemo } from "react";
import { useEffect, useMemo } from "react";
import { useDraft } from "../internal/use-draft";
import { DEFAULT_FUNNEL_STEP_ID } from "./constant";
import { Funnel } from "./funnel";
import { Guard } from "./guard";
import { Step } from "./step";
import type { FunnelOptions, GuardProps, NonEmptyArray, RouteFunnelProps, StepProps } from "./types";
import type {
FunnelOptions,
FunnelStepChangeFunction,
GuardProps,
NonEmptyArray,
RouteFunnelProps,
StepProps,
} from "./types";

export const useCoreFunnel = <Steps extends NonEmptyArray<string>>(options: FunnelOptions<Steps>) => {
const [_step, _setStep] = useDraft(options?.step ?? options?.steps[0]);
export const useCoreFunnel = <Steps extends NonEmptyArray<string>>(
options: FunnelOptions<Steps> & { onStepChange?: FunnelStepChangeFunction<Steps> },
) => {
const [_step, _setStep] = useDraft<Steps[number] | undefined>(options?.step);
const steps = options.steps;
const step = options?.step;
const funnelId = options?.funnelId ?? DEFAULT_FUNNEL_STEP_ID;
const funnelId = options?.funnelId;

const _onStepChange = (param: Steps[number]) => {
const _onStepChange: FunnelStepChangeFunction<Steps> = (param: Steps[number], routeOptions) => {
_setStep(param);
};

const onStepChange = options?.onStepChange ?? _onStepChange;

const FunnelComponent = useMemo(() => {
return Object.assign(
(props: RouteFunnelProps<Steps>) => {
Expand All @@ -32,5 +42,5 @@ export const useCoreFunnel = <Steps extends NonEmptyArray<string>>(options: Funn
);
}, [step, steps]);

return [FunnelComponent, { funnelId, step, onStepChange: _onStepChange }] as const;
return [FunnelComponent, { funnelId, step, onStepChange }] as const;
};
Loading

0 comments on commit 1a12c4e

Please sign in to comment.