w-funnel/src/lib/admin/builder/utils.ts
2025-09-28 22:48:50 +02:00

112 lines
3.6 KiB
TypeScript

import type { BuilderState } from "@/lib/admin/builder/context";
import type { BuilderScreen, BuilderFunnelState } from "@/lib/admin/builder/types";
import type {
FunnelDefinition,
ScreenDefinition,
ListScreenDefinition,
ScreenVariantDefinition,
} from "@/lib/funnel/types";
function deepCloneValue<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => deepCloneValue(item)) as unknown as T;
}
if (value && typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>).map(([key, entryValue]) => [
key,
deepCloneValue(entryValue),
]);
return Object.fromEntries(entries) as T;
}
return value;
}
function withPositions(screens: ScreenDefinition[]): BuilderScreen[] {
return screens.map((screen, index) => ({
...screen,
position: {
x: 120 + (index % 4) * 240,
y: 120 + Math.floor(index / 4) * 200,
},
})) as BuilderScreen[];
}
export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderState {
const builderScreens = withPositions(funnel.screens);
return {
meta: funnel.meta,
defaultTexts: funnel.defaultTexts,
screens: builderScreens,
selectedScreenId: builderScreens[0]?.id ?? null,
isDirty: false,
};
}
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
const screens = state.screens;
const meta: FunnelDefinition["meta"] = {
...state.meta,
firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id,
};
return {
meta,
defaultTexts: state.defaultTexts,
screens,
};
}
export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderScreen>): BuilderScreen {
const copy = {
...screen,
...(screen.template === "list" && 'list' in screen ? {
list: {
...(screen as ListScreenDefinition).list,
options: (screen as ListScreenDefinition).list.options.map((option) => ({ ...option })),
}
} : {}),
...(Array.isArray(screen.variants)
? {
variants: screen.variants.map((variant) => ({
conditions: variant.conditions.map((condition) => ({
screenId: condition.screenId,
operator: condition.operator,
conditionType: condition.conditionType,
...(condition.optionIds ? { optionIds: [...condition.optionIds] } : {}),
...(condition.values ? { values: [...condition.values] } : {}),
})),
...(variant.overrides
? { overrides: deepCloneValue(variant.overrides) as ScreenVariantDefinition<ScreenDefinition>["overrides"] }
: {}),
})),
}
: {}),
navigation: screen.navigation
? {
defaultNextScreenId: screen.navigation.defaultNextScreenId,
rules: screen.navigation.rules?.map((rule) => ({
nextScreenId: rule.nextScreenId,
conditions: rule.conditions.map((condition) => ({
screenId: condition.screenId,
operator: condition.operator,
conditionType: condition.conditionType,
...(condition.optionIds ? { optionIds: [...condition.optionIds] } : {}),
...(condition.values ? { values: [...condition.values] } : {}),
})),
})),
}
: undefined,
} as BuilderScreen;
return overrides ? { ...copy, ...overrides } as BuilderScreen : copy;
}
export function toBuilderFunnelState(state: BuilderState): BuilderFunnelState {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isDirty: _isDirty, selectedScreenId: _selectedScreenId, ...rest } = state;
return rest;
}