) => void}
/>
);
diff --git a/src/components/admin/builder/templates/TextScreenConfig.tsx b/src/components/admin/builder/templates/TextScreenConfig.tsx
index 9cc6c2d..6129090 100644
--- a/src/components/admin/builder/templates/TextScreenConfig.tsx
+++ b/src/components/admin/builder/templates/TextScreenConfig.tsx
@@ -10,7 +10,7 @@ interface TextScreenConfigProps {
}
export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
- const textScreen = screen as TextScreenDefinition & { position: any };
+ const textScreen = screen as TextScreenDefinition & { position: { x: number; y: number } };
return (
@@ -20,10 +20,10 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
onUpdate({
+ onChange={(e) => onUpdate({
title: {
...textScreen.title,
- text: value,
+ text: e.target.value,
font: textScreen.title?.font || "manrope",
weight: textScreen.title?.weight || "bold",
align: textScreen.title?.align || "center",
@@ -39,7 +39,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
title: {
...textScreen.title,
text: textScreen.title?.text || "",
- font: e.target.value as any,
+ font: e.target.value as "manrope" | "inter" | "geistSans" | "geistMono",
}
})}
>
@@ -54,7 +54,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
title: {
...textScreen.title,
text: textScreen.title?.text || "",
- weight: e.target.value as any,
+ weight: e.target.value as "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black",
}
})}
>
@@ -70,7 +70,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
title: {
...textScreen.title,
text: textScreen.title?.text || "",
- align: e.target.value as any,
+ align: e.target.value as "center" | "left" | "right",
}
})}
>
@@ -110,7 +110,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
content: {
...textScreen.content,
text: textScreen.content?.text || "",
- font: e.target.value as any,
+ font: e.target.value as "manrope" | "inter" | "geistSans" | "geistMono",
}
})}
>
@@ -128,7 +128,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
content: {
...textScreen.content,
text: textScreen.content?.text || "",
- weight: e.target.value as any,
+ weight: e.target.value as "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black",
}
})}
>
@@ -149,7 +149,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
content: {
...textScreen.content,
text: textScreen.content?.text || "",
- color: e.target.value as any,
+ color: e.target.value as "default" | "primary" | "secondary" | "accent" | "destructive" | "success" | "muted",
}
})}
>
@@ -168,7 +168,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
content: {
...textScreen.content,
text: textScreen.content?.text || "",
- align: e.target.value as any,
+ align: e.target.value as "center" | "left" | "right",
}
})}
>
@@ -186,9 +186,9 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
onUpdate({
- bottomActionButton: value ? {
- text: value,
+ onChange={(e) => onUpdate({
+ bottomActionButton: e.target.value ? {
+ text: e.target.value,
} : undefined
})}
/>
diff --git a/src/components/funnel/FunnelRuntime.tsx b/src/components/funnel/FunnelRuntime.tsx
index abe7f63..8dbfac8 100644
--- a/src/components/funnel/FunnelRuntime.tsx
+++ b/src/components/funnel/FunnelRuntime.tsx
@@ -181,23 +181,6 @@ function getScreenById(funnel: FunnelDefinition, screenId: string) {
return funnel.screens.find((screen) => screen.id === screenId);
}
-function calculateScreenProgress(
- currentScreenId: string,
- funnel: FunnelDefinition,
- answers: Record
-): { current: number; total: number } {
- // Total is always the same - total number of screens in funnel
- const total = funnel.screens.length;
-
- // Find current screen index in the screens array
- const currentIndex = funnel.screens.findIndex(screen => screen.id === currentScreenId);
- const current = currentIndex >= 0 ? currentIndex + 1 : 1;
-
- return {
- current,
- total,
- };
-}
function getCurrentTemplateRenderer(screen: ScreenDefinition): TemplateRenderer {
const renderer = TEMPLATE_REGISTRY[screen.template];
@@ -221,8 +204,11 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
// Calculate automatic progress
const screenProgress = useMemo(() => {
- return calculateScreenProgress(currentScreen.id, funnel, answers);
- }, [currentScreen.id, funnel, answers]);
+ const total = funnel.screens.length;
+ const currentIndex = funnel.screens.findIndex(screen => screen.id === currentScreen.id);
+ const current = currentIndex >= 0 ? currentIndex + 1 : 1;
+ return { current, total };
+ }, [currentScreen.id, funnel]);
useEffect(() => {
registerScreen(currentScreen.id);
diff --git a/src/components/funnel/templates/InfoTemplate.tsx b/src/components/funnel/templates/InfoTemplate.tsx
index 66718f2..7028463 100644
--- a/src/components/funnel/templates/InfoTemplate.tsx
+++ b/src/components/funnel/templates/InfoTemplate.tsx
@@ -10,7 +10,6 @@ import type { BottomActionButtonProps } from "@/components/widgets/BottomActionB
import Typography from "@/components/ui/Typography/Typography";
import {
- buildLayoutQuestionProps,
buildHeaderProgress,
buildTypographyProps,
shouldShowBackButton,
diff --git a/src/lib/admin/builder/context.tsx b/src/lib/admin/builder/context.tsx
index 56ccb4d..c1cb90b 100644
--- a/src/lib/admin/builder/context.tsx
+++ b/src/lib/admin/builder/context.tsx
@@ -5,7 +5,9 @@ import { createContext, useContext, useMemo, useReducer, type ReactNode } from "
import type {
BuilderFunnelState,
BuilderScreen,
+ BuilderScreenPosition,
} from "@/lib/admin/builder/types";
+import type { ListScreenDefinition } from "@/lib/funnel/types";
import type { NavigationRuleDefinition } from "@/lib/funnel/types";
interface BuilderState extends BuilderFunnelState {
@@ -121,7 +123,7 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
}
case "add-screen": {
const nextId = generateScreenId(state.screens.map((s) => s.id));
- const newScreen: BuilderScreen = {
+ const baseScreen = {
...INITIAL_SCREEN,
id: nextId,
position: {
@@ -129,23 +131,27 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
y: (action.payload?.position?.y ?? 120) + state.screens.length * 20,
},
...action.payload,
- list: {
- ...INITIAL_SCREEN.list,
- ...(action.payload?.list ?? {}),
- options:
- action.payload?.list?.options && action.payload.list.options.length > 0
- ? action.payload.list.options
- : INITIAL_SCREEN.list.options.map((option, index) => ({
- ...option,
- id: `option-${index + 1}`,
- })),
- },
navigation: {
defaultNextScreenId: action.payload?.navigation?.defaultNextScreenId,
rules: action.payload?.navigation?.rules ?? [],
},
};
+ const newScreen: BuilderScreen = action.payload?.template === "list" ? {
+ ...baseScreen,
+ template: "list" as const,
+ list: {
+ selectionType: "single" as const,
+ options: action.payload?.list?.options && action.payload.list.options.length > 0
+ ? action.payload.list.options
+ : [
+ { id: "option-1", label: "Вариант 1" },
+ { id: "option-2", label: "Вариант 2" },
+ ],
+ ...(action.payload?.list ?? {}),
+ },
+ } : baseScreen as BuilderScreen;
+
return withDirty(state, {
...state,
screens: [...state.screens, newScreen],
@@ -182,22 +188,23 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
...state,
screens: state.screens.map((current) =>
current.id === screenId
- ? {
+ ? ({
...current,
...screen,
title: screen.title ? { ...current.title, ...screen.title } : current.title,
- subtitle:
- screen.subtitle !== undefined
- ? screen.subtitle
- : current.subtitle,
- list: screen.list
- ? {
- ...current.list,
- ...screen.list,
- options: screen.list.options ?? current.list.options,
- }
- : current.list,
- }
+ ...(('subtitle' in screen && screen.subtitle !== undefined) ? {
+ subtitle: screen.subtitle
+ } : ('subtitle' in current) ? {
+ subtitle: current.subtitle
+ } : {}),
+ ...(current.template === "list" && 'list' in screen && screen.list ? {
+ list: {
+ ...(current as ListScreenDefinition & { position: BuilderScreenPosition }).list,
+ ...screen.list,
+ options: screen.list.options ?? (current as ListScreenDefinition & { position: BuilderScreenPosition }).list.options,
+ }
+ } : {}),
+ } as BuilderScreen)
: current
),
});
diff --git a/src/lib/admin/builder/templates.ts b/src/lib/admin/builder/templates.ts
index 20a801f..42c555a 100644
--- a/src/lib/admin/builder/templates.ts
+++ b/src/lib/admin/builder/templates.ts
@@ -24,9 +24,9 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
id: "list",
label: "Вопрос с вариантами",
create: ({ screenId, position }, overrides) => {
- const base: BuilderScreen = {
+ const base = {
id: screenId,
- template: "list",
+ template: "list" as const,
header: {
progress: {
current: 1,
@@ -36,16 +36,16 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
},
title: {
text: "Новый экран",
- font: "manrope",
- weight: "bold",
+ font: "manrope" as const,
+ weight: "bold" as const,
},
subtitle: {
text: "Опишите вопрос справа",
- color: "muted",
- font: "inter",
+ color: "muted" as const,
+ font: "inter" as const,
},
list: {
- selectionType: "single",
+ selectionType: "single" as const,
options: cloneOptions([
{ id: "option-1", label: "Вариант 1" },
{ id: "option-2", label: "Вариант 2" },
@@ -59,13 +59,13 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
};
if (!overrides) {
- return base;
+ return base as BuilderScreen;
}
return {
...base,
...overrides,
- list: overrides.list
+ list: ('list' in overrides && overrides.list)
? {
...base.list,
...overrides.list,
@@ -79,7 +79,7 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
rules: overrides.navigation.rules ?? base.navigation?.rules ?? [],
}
: base.navigation,
- };
+ } as BuilderScreen;
},
};
diff --git a/src/lib/admin/builder/utils.ts b/src/lib/admin/builder/utils.ts
index 1222e7d..0b7625d 100644
--- a/src/lib/admin/builder/utils.ts
+++ b/src/lib/admin/builder/utils.ts
@@ -1,15 +1,15 @@
import type { BuilderState } from "@/lib/admin/builder/context";
-import type { BuilderScreen, BuilderFunnelState } from "@/lib/admin/builder/types";
-import type { FunnelDefinition, ListScreenDefinition } from "@/lib/funnel/types";
+import type { BuilderScreen, BuilderFunnelState, BuilderScreenPosition } from "@/lib/admin/builder/types";
+import type { FunnelDefinition, ScreenDefinition, ListScreenDefinition } from "@/lib/funnel/types";
-function withPositions(screens: ListScreenDefinition[]): BuilderScreen[] {
+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 {
@@ -24,7 +24,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
}
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
- const screens = state.screens.map(({ position, ...rest }) => rest);
+ const screens = state.screens.map(({ position: _position, ...rest }) => rest);
const meta: FunnelDefinition["meta"] = {
...state.meta,
firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id,
@@ -37,13 +37,15 @@ export function serializeBuilderState(state: BuilderFunnelState): FunnelDefiniti
}
export function cloneScreen(screen: BuilderScreen, overrides?: Partial): BuilderScreen {
- const copy: BuilderScreen = {
+ const copy = {
...screen,
position: { ...screen.position },
- list: {
- ...screen.list,
- options: screen.list.options.map((option) => ({ ...option })),
- },
+ ...(screen.template === "list" && 'list' in screen ? {
+ list: {
+ ...(screen as ListScreenDefinition & { position: BuilderScreenPosition }).list,
+ options: (screen as ListScreenDefinition & { position: BuilderScreenPosition }).list.options.map((option) => ({ ...option })),
+ }
+ } : {}),
navigation: screen.navigation
? {
defaultNextScreenId: screen.navigation.defaultNextScreenId,
@@ -57,12 +59,12 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial option.id));
+ const screenWithList = screen as { list: { options: { id: string }[] } };
+ const duplicates = collectDuplicateIds(screenWithList.list.options.map((option) => option.id));
for (const duplicateId of duplicates) {
issues.push(
createIssue(
@@ -136,8 +136,8 @@ function validateNavigation(screen: BuilderScreen, state: BuilderState, issues:
continue;
}
- const referenceScreenWithList = referenceScreen as any;
- const availableOptionIds = new Set(referenceScreenWithList.list.options.map((option: any) => option.id));
+ const referenceScreenWithList = referenceScreen as { list: { options: { id: string }[] } };
+ const availableOptionIds = new Set(referenceScreenWithList.list.options.map((option) => option.id));
const missingOptionIds = (condition.optionIds ?? []).filter((optionId) => !availableOptionIds.has(optionId));
if (missingOptionIds.length > 0) {
issues.push(