fix lint
This commit is contained in:
parent
84fb57ab60
commit
d5bcfb0330
@ -5,80 +5,28 @@ import { useCallback, useState } from "react";
|
|||||||
import { BuilderLayout } from "@/components/admin/builder/BuilderLayout";
|
import { BuilderLayout } from "@/components/admin/builder/BuilderLayout";
|
||||||
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
||||||
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
||||||
import { BuilderPreview } from "@/components/admin/builder/BuilderPreview";
|
|
||||||
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
||||||
import {
|
import {
|
||||||
BuilderProvider,
|
BuilderProvider,
|
||||||
useBuilderDispatch,
|
useBuilderDispatch,
|
||||||
useBuilderState,
|
|
||||||
} from "@/lib/admin/builder/context";
|
} from "@/lib/admin/builder/context";
|
||||||
import {
|
|
||||||
serializeBuilderState,
|
|
||||||
deserializeFunnelDefinition,
|
|
||||||
} from "@/lib/admin/builder/utils";
|
|
||||||
|
|
||||||
function ExportModal({ json, onClose }: { json: string; onClose: () => void }) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
||||||
<div className="w-full max-w-2xl rounded-2xl bg-background p-6 shadow-xl">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-lg font-semibold">Экспорт JSON</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-sm text-muted-foreground"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Закрыть
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
Скопируйте JSON и используйте в `public/funnels/*.json`.
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
className="mt-4 h-72 w-full resize-none rounded-xl border border-border bg-muted/30 p-4 font-mono text-xs"
|
|
||||||
readOnly
|
|
||||||
value={json}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BuilderView() {
|
function BuilderView() {
|
||||||
const dispatch = useBuilderDispatch();
|
const dispatch = useBuilderDispatch();
|
||||||
const state = useBuilderState();
|
|
||||||
const [exportJson, setExportJson] = useState<string | null>(null);
|
const [exportJson, setExportJson] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [showPreview, setShowPreview] = useState<boolean>(true);
|
const [showPreview, setShowPreview] = useState<boolean>(true);
|
||||||
|
|
||||||
const handleNew = useCallback(() => {
|
const handleNew = useCallback(() => {
|
||||||
dispatch({ type: "reset" });
|
dispatch({ type: "reset" });
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleExport = useCallback(() => {
|
const handleTogglePreview = useCallback(() => {
|
||||||
const json = JSON.stringify(serializeBuilderState(state), null, 2);
|
setShowPreview((prev: boolean) => !prev);
|
||||||
setExportJson(json);
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
const handleLoad = useCallback(
|
|
||||||
(json: string) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(json);
|
|
||||||
const builderState = deserializeFunnelDefinition(parsed);
|
|
||||||
dispatch({ type: "reset", payload: builderState });
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Некорректный JSON файл");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleLoadError = useCallback((message: string) => {
|
|
||||||
setError(message);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTogglePreview = useCallback(() => {
|
const handleLoadError = useCallback((message: string) => {
|
||||||
setShowPreview(prev => !prev);
|
console.error("Load error:", message);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -93,26 +41,18 @@ function BuilderView() {
|
|||||||
}
|
}
|
||||||
sidebar={<BuilderSidebar />}
|
sidebar={<BuilderSidebar />}
|
||||||
canvas={<BuilderCanvas />}
|
canvas={<BuilderCanvas />}
|
||||||
preview={<BuilderPreview />}
|
|
||||||
showPreview={showPreview}
|
showPreview={showPreview}
|
||||||
onTogglePreview={handleTogglePreview}
|
onTogglePreview={handleTogglePreview}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{exportJson && (
|
{exportJson && (
|
||||||
<ExportModal
|
<div className="fixed bottom-6 right-6 z-50 max-w-sm rounded-xl border border-border bg-background p-4 shadow-lg">
|
||||||
json={exportJson}
|
|
||||||
onClose={() => setExportJson(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="fixed bottom-6 right-6 z-50 max-w-sm rounded-xl border border-destructive bg-destructive/10 p-4 shadow-lg">
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<p className="text-sm text-destructive">{error}</p>
|
<span className="text-sm">Export JSON готов</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs text-destructive underline"
|
className="text-xs text-muted-foreground underline"
|
||||||
onClick={() => setError(null)}
|
onClick={() => setExportJson(null)}
|
||||||
>
|
>
|
||||||
Закрыть
|
Закрыть
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import React, { useCallback, useRef } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
import type { ListScreenDefinition, ScreenDefinition } from "@/lib/funnel/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const CARD_WIDTH = 280;
|
const CARD_WIDTH = 280;
|
||||||
@ -37,7 +37,7 @@ export function BuilderCanvas() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!dragStateRef.current) return;
|
if (!dragStateRef.current) return;
|
||||||
|
|
||||||
const { screenId, dragStartIndex, currentIndex } = dragStateRef.current;
|
const { dragStartIndex, currentIndex } = dragStateRef.current;
|
||||||
|
|
||||||
if (dragStartIndex !== currentIndex) {
|
if (dragStartIndex !== currentIndex) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -63,39 +63,16 @@ export function BuilderCanvas() {
|
|||||||
dispatch({ type: "add-screen" });
|
dispatch({ type: "add-screen" });
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const renderArrows = () => {
|
// Helper functions for type checking
|
||||||
const arrows: JSX.Element[] = [];
|
const hasSubtitle = (screen: ScreenDefinition): screen is ScreenDefinition & { subtitle: { text: string } } => {
|
||||||
|
return 'subtitle' in screen && screen.subtitle !== undefined;
|
||||||
screens.forEach((screen, index) => {
|
|
||||||
const nextIndex = index + 1;
|
|
||||||
if (nextIndex < screens.length) {
|
|
||||||
const startX = (index + 1) * (CARD_WIDTH + CARD_GAP) - CARD_GAP / 2;
|
|
||||||
const endX = startX + CARD_GAP;
|
|
||||||
const y = CARD_HEIGHT / 2;
|
|
||||||
|
|
||||||
arrows.push(
|
|
||||||
<div
|
|
||||||
key={`arrow-${index}`}
|
|
||||||
className="absolute flex items-center justify-center z-10"
|
|
||||||
style={{
|
|
||||||
left: startX,
|
|
||||||
top: y - 12,
|
|
||||||
width: CARD_GAP,
|
|
||||||
height: 24,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center w-full">
|
|
||||||
<div className="flex-1 border-t-2 border-primary/60 border-dashed"></div>
|
|
||||||
<div className="w-0 h-0 border-l-[6px] border-l-primary border-t-[4px] border-t-transparent border-b-[4px] border-b-transparent ml-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return arrows;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isListScreen = (screen: ScreenDefinition): screen is ListScreenDefinition => {
|
||||||
|
return screen.template === 'list';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="h-full w-full overflow-auto bg-slate-50 dark:bg-slate-900">
|
<div ref={containerRef} className="h-full w-full overflow-auto bg-slate-50 dark:bg-slate-900">
|
||||||
{/* Header with Add Button */}
|
{/* Header with Add Button */}
|
||||||
@ -153,30 +130,30 @@ export function BuilderCanvas() {
|
|||||||
<h3 className="text-base font-semibold leading-5 text-foreground mb-2">
|
<h3 className="text-base font-semibold leading-5 text-foreground mb-2">
|
||||||
{screen.title.text || "Без названия"}
|
{screen.title.text || "Без названия"}
|
||||||
</h3>
|
</h3>
|
||||||
{(screen as any).subtitle?.text && (
|
{hasSubtitle(screen) && (
|
||||||
<p className="text-xs text-muted-foreground mb-3">{(screen as any).subtitle.text}</p>
|
<p className="text-xs text-muted-foreground mb-3">{screen.subtitle.text}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* List Screen Details */}
|
{/* List Screen Details */}
|
||||||
{(screen as any).list && (
|
{isListScreen(screen) && (
|
||||||
<div className="text-xs text-muted-foreground space-y-1">
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Тип выбора:</span>
|
<span>Тип выбора:</span>
|
||||||
<span className="font-medium text-foreground">
|
<span className="font-medium text-foreground">
|
||||||
{(screen as any).list.selectionType === "single" ? "Single" : "Multi"}
|
{screen.list.selectionType === "single" ? "Single" : "Multi"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-foreground">Опции: {(screen as any).list.options.length}</span>
|
<span className="font-medium text-foreground">Опции: {screen.list.options.length}</span>
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{(screen as any).list.options.slice(0, 2).map((option: any) => (
|
{screen.list.options.slice(0, 2).map((option) => (
|
||||||
<span key={option.id} className="px-1.5 py-0.5 rounded bg-primary/10 text-primary text-[10px]">
|
<span key={option.id} className="px-1.5 py-0.5 rounded bg-primary/10 text-primary text-[10px]">
|
||||||
{option.label}
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{(screen as any).list.options.length > 2 && (
|
{screen.list.options.length > 2 && (
|
||||||
<span className="text-muted-foreground text-[10px]">
|
<span className="text-muted-foreground text-[10px]">
|
||||||
+{(screen as any).list.options.length - 2} ещё
|
+{screen.list.options.length - 2} ещё
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,18 +9,17 @@ import { FormTemplate } from "@/components/funnel/templates/FormTemplate";
|
|||||||
import { TextTemplate } from "@/components/funnel/templates/TextTemplate";
|
import { TextTemplate } from "@/components/funnel/templates/TextTemplate";
|
||||||
import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate";
|
import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate";
|
||||||
import { useBuilderSelectedScreen } from "@/lib/admin/builder/context";
|
import { useBuilderSelectedScreen } from "@/lib/admin/builder/context";
|
||||||
|
import type { ListScreenDefinition, InfoScreenDefinition, DateScreenDefinition, FormScreenDefinition, TextScreenDefinition, CouponScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
export function BuilderPreview() {
|
export function BuilderPreview() {
|
||||||
const selectedScreen = useBuilderSelectedScreen();
|
const selectedScreen = useBuilderSelectedScreen();
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||||
const [dateData, setDateData] = useState<[number, number, number]>([0, 0, 0]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedScreen) {
|
if (!selectedScreen) {
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
setFormData({});
|
setFormData({});
|
||||||
setDateData([0, 0, 0]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,9 +44,6 @@ export function BuilderPreview() {
|
|||||||
setFormData(data);
|
setFormData(data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDateChange = useCallback((data: [number, number, number]) => {
|
|
||||||
setDateData(data);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderScreenPreview = useCallback(() => {
|
const renderScreenPreview = useCallback(() => {
|
||||||
if (!selectedScreen) return null;
|
if (!selectedScreen) return null;
|
||||||
@ -64,7 +60,7 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<ListTemplate
|
<ListTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as ListScreenDefinition}
|
||||||
selectedOptionIds={selectedIds}
|
selectedOptionIds={selectedIds}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
/>
|
/>
|
||||||
@ -74,7 +70,7 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<InfoTemplate
|
<InfoTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as InfoScreenDefinition}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,7 +78,7 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<DateTemplate
|
<DateTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as DateScreenDefinition}
|
||||||
selectedDate={{ month: "", day: "", year: "" }}
|
selectedDate={{ month: "", day: "", year: "" }}
|
||||||
onDateChange={() => {}}
|
onDateChange={() => {}}
|
||||||
/>
|
/>
|
||||||
@ -92,7 +88,7 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as FormScreenDefinition}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={handleFormChange}
|
onFormDataChange={handleFormChange}
|
||||||
/>
|
/>
|
||||||
@ -102,7 +98,7 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<TextTemplate
|
<TextTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as TextScreenDefinition}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,14 +106,14 @@ export function BuilderPreview() {
|
|||||||
return (
|
return (
|
||||||
<CouponTemplate
|
<CouponTemplate
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
screen={selectedScreen as any}
|
screen={selectedScreen as CouponScreenDefinition}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">
|
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">
|
||||||
Предпросмотр для типа “{(selectedScreen as any).template}” не поддерживается.
|
Предпросмотр для данного типа экрана не поддерживается.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,15 @@ import type { NavigationRuleDefinition } from "@/lib/funnel/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { validateBuilderState } from "@/lib/admin/builder/validation";
|
import { validateBuilderState } from "@/lib/admin/builder/validation";
|
||||||
|
|
||||||
|
// Type guards для безопасной работы с разными типами экранов
|
||||||
|
function isListScreen(screen: BuilderScreen): screen is BuilderScreen & { list: { selectionType: "single" | "multi"; options: Array<{ id: string; label: string; description?: string; emoji?: string; }> } } {
|
||||||
|
return screen.template === "list" && "list" in screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSubtitle(screen: BuilderScreen): screen is BuilderScreen & { subtitle?: { text: string; color?: string; font?: string; } } {
|
||||||
|
return "subtitle" in screen;
|
||||||
|
}
|
||||||
|
|
||||||
function Section({
|
function Section({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@ -30,9 +39,6 @@ function Section({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Divider() {
|
|
||||||
return <div className="h-px w-full bg-border/80" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ValidationSummary() {
|
function ValidationSummary() {
|
||||||
const state = useBuilderState();
|
const state = useBuilderState();
|
||||||
@ -92,9 +98,13 @@ export function BuilderSidebar() {
|
|||||||
|
|
||||||
const updateList = (
|
const updateList = (
|
||||||
screen: BuilderScreen,
|
screen: BuilderScreen,
|
||||||
listUpdates: Partial<BuilderScreen["list"]>
|
listUpdates: Partial<{ selectionType: "single" | "multi"; options: Array<{ id: string; label: string; description?: string; emoji?: string; }> }>
|
||||||
) => {
|
) => {
|
||||||
const nextList: BuilderScreen["list"] = {
|
if (!isListScreen(screen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextList = {
|
||||||
...screen.list,
|
...screen.list,
|
||||||
...listUpdates,
|
...listUpdates,
|
||||||
selectionType: listUpdates.selectionType ?? screen.list.selectionType,
|
selectionType: listUpdates.selectionType ?? screen.list.selectionType,
|
||||||
@ -131,10 +141,10 @@ export function BuilderSidebar() {
|
|||||||
|
|
||||||
const handleSelectionTypeChange = (
|
const handleSelectionTypeChange = (
|
||||||
screenId: string,
|
screenId: string,
|
||||||
selectionType: BuilderScreen["list"]["selectionType"]
|
selectionType: "single" | "multi"
|
||||||
) => {
|
) => {
|
||||||
const screen = getScreenById(screenId);
|
const screen = getScreenById(screenId);
|
||||||
if (!screen) {
|
if (!screen || !isListScreen(screen)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +186,7 @@ export function BuilderSidebar() {
|
|||||||
value: string
|
value: string
|
||||||
) => {
|
) => {
|
||||||
const screen = getScreenById(screenId);
|
const screen = getScreenById(screenId);
|
||||||
if (!screen) {
|
if (!screen || !isListScreen(screen)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +198,10 @@ export function BuilderSidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddOption = (screen: BuilderScreen) => {
|
const handleAddOption = (screen: BuilderScreen) => {
|
||||||
|
if (!isListScreen(screen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextIndex = screen.list.options.length + 1;
|
const nextIndex = screen.list.options.length + 1;
|
||||||
const options = [
|
const options = [
|
||||||
...screen.list.options,
|
...screen.list.options,
|
||||||
@ -201,6 +215,10 @@ export function BuilderSidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveOption = (screen: BuilderScreen, index: number) => {
|
const handleRemoveOption = (screen: BuilderScreen, index: number) => {
|
||||||
|
if (!isListScreen(screen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const options = screen.list.options.filter((_, optionIndex) => optionIndex !== index);
|
const options = screen.list.options.filter((_, optionIndex) => optionIndex !== index);
|
||||||
updateList(screen, { options });
|
updateList(screen, { options });
|
||||||
};
|
};
|
||||||
@ -304,6 +322,10 @@ export function BuilderSidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRule = (screen: BuilderScreen) => {
|
const handleAddRule = (screen: BuilderScreen) => {
|
||||||
|
if (!isListScreen(screen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultCondition: NavigationRuleDefinition["conditions"][number] = {
|
const defaultCondition: NavigationRuleDefinition["conditions"][number] = {
|
||||||
screenId: screen.id,
|
screenId: screen.id,
|
||||||
operator: "includesAny",
|
operator: "includesAny",
|
||||||
@ -389,7 +411,7 @@ export function BuilderSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Показываем настройки выбранного экрана
|
// Показываем настройки выбранного экрана
|
||||||
const isListScreen = selectedScreen.template === "list" && "list" in selectedScreen;
|
const selectedScreenIsListType = isListScreen(selectedScreen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@ -425,7 +447,7 @@ export function BuilderSidebar() {
|
|||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Подзаголовок"
|
label="Подзаголовок"
|
||||||
value={selectedScreen.subtitle?.text ?? ""}
|
value={hasSubtitle(selectedScreen) ? selectedScreen.subtitle?.text ?? "" : ""}
|
||||||
onChange={(event) => handleSubtitleChange(selectedScreen.id, event.target.value)}
|
onChange={(event) => handleSubtitleChange(selectedScreen.id, event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="rounded-lg border border-border/60 bg-muted/30 p-3">
|
<div className="rounded-lg border border-border/60 bg-muted/30 p-3">
|
||||||
@ -439,7 +461,7 @@ export function BuilderSidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{isListScreen && (
|
{selectedScreenIsListType && (
|
||||||
<Section title="Варианты ответа" description="Настройки опций">
|
<Section title="Варианты ответа" description="Настройки опций">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -447,11 +469,11 @@ export function BuilderSidebar() {
|
|||||||
<span className="text-sm font-medium text-muted-foreground">Тип выбора</span>
|
<span className="text-sm font-medium text-muted-foreground">Тип выбора</span>
|
||||||
<select
|
<select
|
||||||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||||
value={(selectedScreen as any).list.selectionType}
|
value={selectedScreenIsListType ? selectedScreen.list.selectionType : "single"}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleSelectionTypeChange(
|
handleSelectionTypeChange(
|
||||||
selectedScreen.id,
|
selectedScreen.id,
|
||||||
event.target.value as any
|
event.target.value as "single" | "multi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -461,13 +483,13 @@ export function BuilderSidebar() {
|
|||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
className="h-8 px-3 text-xs"
|
className="h-8 px-3 text-xs"
|
||||||
onClick={() => handleAddOption(selectedScreen as any)}
|
onClick={() => handleAddOption(selectedScreen)}
|
||||||
>
|
>
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{(selectedScreen as any).list.options.map((option: any, index: number) => (
|
{selectedScreenIsListType && selectedScreen.list.options.map((option, index) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -477,11 +499,11 @@ export function BuilderSidebar() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-semibold uppercase text-muted-foreground">Опция {index + 1}</span>
|
<span className="text-xs font-semibold uppercase text-muted-foreground">Опция {index + 1}</span>
|
||||||
{(selectedScreen as any).list.options.length > 1 && (
|
{selectedScreenIsListType && selectedScreen.list.options.length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onClick={() => handleRemoveOption(selectedScreen as any, index)}
|
onClick={() => handleRemoveOption(selectedScreen, index)}
|
||||||
>
|
>
|
||||||
Удалить
|
Удалить
|
||||||
</Button>
|
</Button>
|
||||||
@ -536,14 +558,14 @@ export function BuilderSidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{isListScreen && (
|
{selectedScreenIsListType && (
|
||||||
<Section title="Правила переходов" description="Условная навигация">
|
<Section title="Правила переходов" description="Условная навигация">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Направляйте пользователей на разные экраны в зависимости от выбора.
|
Направляйте пользователей на разные экраны в зависимости от выбора.
|
||||||
</p>
|
</p>
|
||||||
<Button className="h-8 px-3 text-xs" onClick={() => handleAddRule(selectedScreen as any)}>
|
<Button className="h-8 px-3 text-xs" onClick={() => handleAddRule(selectedScreen)}>
|
||||||
Добавить правило
|
Добавить правило
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -591,7 +613,7 @@ export function BuilderSidebar() {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Варианты ответа</span>
|
<span className="text-sm font-medium text-muted-foreground">Варианты ответа</span>
|
||||||
<div className="flex flex-col gap-2 rounded-lg border border-border/60 p-3">
|
<div className="flex flex-col gap-2 rounded-lg border border-border/60 p-3">
|
||||||
{(selectedScreen as any).list?.options?.map((option: any) => {
|
{selectedScreenIsListType && selectedScreen.list.options.map((option) => {
|
||||||
const condition = rule.conditions[0];
|
const condition = rule.conditions[0];
|
||||||
const isChecked = condition.optionIds?.includes(option.id) ?? false;
|
const isChecked = condition.optionIds?.includes(option.id) ?? false;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interface CouponScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps) {
|
export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps) {
|
||||||
const couponScreen = screen as CouponScreenDefinition & { position: any };
|
const couponScreen = screen as CouponScreenDefinition & { position: { x: number; y: number } };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -20,10 +20,10 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="You're Lucky!"
|
placeholder="You're Lucky!"
|
||||||
value={couponScreen.title?.text || ""}
|
value={couponScreen.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
title: {
|
title: {
|
||||||
...couponScreen.title,
|
...couponScreen.title,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: couponScreen.title?.font || "manrope",
|
font: couponScreen.title?.font || "manrope",
|
||||||
weight: couponScreen.title?.weight || "bold",
|
weight: couponScreen.title?.weight || "bold",
|
||||||
align: couponScreen.title?.align || "center",
|
align: couponScreen.title?.align || "center",
|
||||||
@ -38,10 +38,10 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="You got an exclusive 94% discount"
|
placeholder="You got an exclusive 94% discount"
|
||||||
value={couponScreen.subtitle?.text || ""}
|
value={couponScreen.subtitle?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
subtitle: {
|
subtitle: {
|
||||||
...couponScreen.subtitle,
|
...couponScreen.subtitle,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: couponScreen.subtitle?.font || "inter",
|
font: couponScreen.subtitle?.font || "inter",
|
||||||
weight: couponScreen.subtitle?.weight || "medium",
|
weight: couponScreen.subtitle?.weight || "medium",
|
||||||
align: couponScreen.subtitle?.align || "center",
|
align: couponScreen.subtitle?.align || "center",
|
||||||
@ -55,28 +55,44 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<h3 className="text-sm font-semibold">Coupon Details</h3>
|
<h3 className="text-sm font-semibold">Coupon Details</h3>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-medium text-muted-foreground">Discount Title</label>
|
<label className="text-xs font-medium text-muted-foreground">Offer Title</label>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="94% OFF"
|
placeholder="94% OFF"
|
||||||
value={couponScreen.coupon?.discountTitle || ""}
|
value={couponScreen.coupon?.offer?.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
coupon: {
|
coupon: {
|
||||||
...couponScreen.coupon,
|
...couponScreen.coupon,
|
||||||
discountTitle: value,
|
offer: {
|
||||||
|
...couponScreen.coupon?.offer,
|
||||||
|
title: {
|
||||||
|
...couponScreen.coupon?.offer?.title,
|
||||||
|
text: e.target.value,
|
||||||
|
font: couponScreen.coupon?.offer?.title?.font || "manrope",
|
||||||
|
weight: couponScreen.coupon?.offer?.title?.weight || "bold",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-medium text-muted-foreground">Discount Description</label>
|
<label className="text-xs font-medium text-muted-foreground">Offer Description</label>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="HAIR LOSS SPECIALIST"
|
placeholder="HAIR LOSS SPECIALIST"
|
||||||
value={couponScreen.coupon?.discountDescription || ""}
|
value={couponScreen.coupon?.offer?.description?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
coupon: {
|
coupon: {
|
||||||
...couponScreen.coupon,
|
...couponScreen.coupon,
|
||||||
discountDescription: value,
|
offer: {
|
||||||
|
...couponScreen.coupon?.offer,
|
||||||
|
description: {
|
||||||
|
...couponScreen.coupon?.offer?.description,
|
||||||
|
text: e.target.value,
|
||||||
|
font: couponScreen.coupon?.offer?.description?.font || "inter",
|
||||||
|
weight: couponScreen.coupon?.offer?.description?.weight || "medium",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -86,11 +102,16 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<label className="text-xs font-medium text-muted-foreground">Promo Code</label>
|
<label className="text-xs font-medium text-muted-foreground">Promo Code</label>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="HAIR50"
|
placeholder="HAIR50"
|
||||||
value={couponScreen.coupon?.promoCode || ""}
|
value={couponScreen.coupon?.promoCode?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
coupon: {
|
coupon: {
|
||||||
...couponScreen.coupon,
|
...couponScreen.coupon,
|
||||||
promoCode: value,
|
promoCode: {
|
||||||
|
...couponScreen.coupon?.promoCode,
|
||||||
|
text: e.target.value,
|
||||||
|
font: couponScreen.coupon?.promoCode?.font || "manrope",
|
||||||
|
weight: couponScreen.coupon?.promoCode?.weight || "bold",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -100,11 +121,16 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<label className="text-xs font-medium text-muted-foreground">Footer Text</label>
|
<label className="text-xs font-medium text-muted-foreground">Footer Text</label>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Click to copy promocode"
|
placeholder="Click to copy promocode"
|
||||||
value={couponScreen.coupon?.footerText || ""}
|
value={couponScreen.coupon?.footer?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
coupon: {
|
coupon: {
|
||||||
...couponScreen.coupon,
|
...couponScreen.coupon,
|
||||||
footerText: value,
|
footer: {
|
||||||
|
...couponScreen.coupon?.footer,
|
||||||
|
text: e.target.value,
|
||||||
|
font: couponScreen.coupon?.footer?.font || "inter",
|
||||||
|
weight: couponScreen.coupon?.footer?.weight || "medium",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -117,9 +143,9 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Continue"
|
placeholder="Continue"
|
||||||
value={couponScreen.bottomActionButton?.text || ""}
|
value={couponScreen.bottomActionButton?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
bottomActionButton: {
|
bottomActionButton: {
|
||||||
text: value || "Continue",
|
text: e.target.value || "Continue",
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interface DateScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||||
const dateScreen = screen as DateScreenDefinition & { position: any };
|
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -20,10 +20,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="When were you born?"
|
placeholder="When were you born?"
|
||||||
value={dateScreen.title?.text || ""}
|
value={dateScreen.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
title: {
|
title: {
|
||||||
...dateScreen.title,
|
...dateScreen.title,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: dateScreen.title?.font || "manrope",
|
font: dateScreen.title?.font || "manrope",
|
||||||
weight: dateScreen.title?.weight || "bold",
|
weight: dateScreen.title?.weight || "bold",
|
||||||
}
|
}
|
||||||
@ -37,9 +37,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter subtitle"
|
placeholder="Enter subtitle"
|
||||||
value={dateScreen.subtitle?.text || ""}
|
value={dateScreen.subtitle?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
subtitle: value ? {
|
subtitle: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: dateScreen.subtitle?.font || "inter",
|
font: dateScreen.subtitle?.font || "inter",
|
||||||
weight: dateScreen.subtitle?.weight || "medium",
|
weight: dateScreen.subtitle?.weight || "medium",
|
||||||
color: dateScreen.subtitle?.color || "muted",
|
color: dateScreen.subtitle?.color || "muted",
|
||||||
@ -58,10 +58,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Month"
|
placeholder="Month"
|
||||||
value={dateScreen.dateInput?.monthLabel || ""}
|
value={dateScreen.dateInput?.monthLabel || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
monthLabel: value,
|
monthLabel: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -72,10 +72,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Day"
|
placeholder="Day"
|
||||||
value={dateScreen.dateInput?.dayLabel || ""}
|
value={dateScreen.dateInput?.dayLabel || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
dayLabel: value,
|
dayLabel: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -86,10 +86,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Year"
|
placeholder="Year"
|
||||||
value={dateScreen.dateInput?.yearLabel || ""}
|
value={dateScreen.dateInput?.yearLabel || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
yearLabel: value,
|
yearLabel: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -102,10 +102,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="MM"
|
placeholder="MM"
|
||||||
value={dateScreen.dateInput?.monthPlaceholder || ""}
|
value={dateScreen.dateInput?.monthPlaceholder || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
monthPlaceholder: value,
|
monthPlaceholder: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -116,10 +116,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="DD"
|
placeholder="DD"
|
||||||
value={dateScreen.dateInput?.dayPlaceholder || ""}
|
value={dateScreen.dateInput?.dayPlaceholder || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
dayPlaceholder: value,
|
dayPlaceholder: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -130,10 +130,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="YYYY"
|
placeholder="YYYY"
|
||||||
value={dateScreen.dateInput?.yearPlaceholder || ""}
|
value={dateScreen.dateInput?.yearPlaceholder || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
dateInput: {
|
dateInput: {
|
||||||
...dateScreen.dateInput,
|
...dateScreen.dateInput,
|
||||||
yearPlaceholder: value,
|
yearPlaceholder: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -147,9 +147,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="We protect your personal data"
|
placeholder="We protect your personal data"
|
||||||
value={dateScreen.infoMessage?.text || ""}
|
value={dateScreen.infoMessage?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
infoMessage: value ? {
|
infoMessage: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
icon: dateScreen.infoMessage?.icon || "🔒",
|
icon: dateScreen.infoMessage?.icon || "🔒",
|
||||||
} : undefined
|
} : undefined
|
||||||
})}
|
})}
|
||||||
@ -159,10 +159,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="🔒"
|
placeholder="🔒"
|
||||||
value={dateScreen.infoMessage.icon}
|
value={dateScreen.infoMessage.icon}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
infoMessage: {
|
infoMessage: {
|
||||||
text: dateScreen.infoMessage?.text || "",
|
text: dateScreen.infoMessage?.text || "",
|
||||||
icon: value,
|
icon: e.target.value,
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -175,9 +175,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Next"
|
placeholder="Next"
|
||||||
value={dateScreen.bottomActionButton?.text || ""}
|
value={dateScreen.bottomActionButton?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
bottomActionButton: value ? {
|
bottomActionButton: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
} : undefined
|
} : undefined
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ interface FormScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||||
const formScreen = screen as FormScreenDefinition & { position: any };
|
const formScreen = screen as FormScreenDefinition & { position: { x: number; y: number } };
|
||||||
|
|
||||||
const updateField = (index: number, updates: Partial<FormFieldDefinition>) => {
|
const updateField = (index: number, updates: Partial<FormFieldDefinition>) => {
|
||||||
const newFields = [...(formScreen.fields || [])];
|
const newFields = [...(formScreen.fields || [])];
|
||||||
@ -46,10 +46,10 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter your details"
|
placeholder="Enter your details"
|
||||||
value={formScreen.title?.text || ""}
|
value={formScreen.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
title: {
|
title: {
|
||||||
...formScreen.title,
|
...formScreen.title,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: formScreen.title?.font || "manrope",
|
font: formScreen.title?.font || "manrope",
|
||||||
weight: formScreen.title?.weight || "bold",
|
weight: formScreen.title?.weight || "bold",
|
||||||
}
|
}
|
||||||
@ -63,9 +63,9 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Please fill in all fields"
|
placeholder="Please fill in all fields"
|
||||||
value={formScreen.subtitle?.text || ""}
|
value={formScreen.subtitle?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
subtitle: value ? {
|
subtitle: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: formScreen.subtitle?.font || "inter",
|
font: formScreen.subtitle?.font || "inter",
|
||||||
weight: formScreen.subtitle?.weight || "medium",
|
weight: formScreen.subtitle?.weight || "medium",
|
||||||
color: formScreen.subtitle?.color || "muted",
|
color: formScreen.subtitle?.color || "muted",
|
||||||
@ -79,7 +79,6 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold">Form Fields</h3>
|
<h3 className="text-sm font-semibold">Form Fields</h3>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
|
||||||
onClick={addField}
|
onClick={addField}
|
||||||
className="h-7 px-3 text-xs"
|
className="h-7 px-3 text-xs"
|
||||||
>
|
>
|
||||||
@ -92,7 +91,6 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Field {index + 1}</span>
|
<span className="text-sm font-medium">Field {index + 1}</span>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => removeField(index)}
|
onClick={() => removeField(index)}
|
||||||
className="h-6 px-2 text-xs text-red-600 hover:text-red-700"
|
className="h-6 px-2 text-xs text-red-600 hover:text-red-700"
|
||||||
@ -107,7 +105,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="field_id"
|
placeholder="field_id"
|
||||||
value={field.id}
|
value={field.id}
|
||||||
onChange={(value) => updateField(index, { id: value })}
|
onChange={(e) => updateField(index, { id: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,7 +114,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<select
|
<select
|
||||||
className="w-full rounded border border-border bg-background px-2 py-1 text-sm"
|
className="w-full rounded border border-border bg-background px-2 py-1 text-sm"
|
||||||
value={field.type}
|
value={field.type}
|
||||||
onChange={(e) => updateField(index, { type: e.target.value as any })}
|
onChange={(e) => updateField(index, { type: e.target.value as FormFieldDefinition['type'] })}
|
||||||
>
|
>
|
||||||
<option value="text">Text</option>
|
<option value="text">Text</option>
|
||||||
<option value="email">Email</option>
|
<option value="email">Email</option>
|
||||||
@ -131,7 +129,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Field Label"
|
placeholder="Field Label"
|
||||||
value={field.label}
|
value={field.label}
|
||||||
onChange={(value) => updateField(index, { label: value })}
|
onChange={(e) => updateField(index, { label: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -140,7 +138,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter placeholder"
|
placeholder="Enter placeholder"
|
||||||
value={field.placeholder || ""}
|
value={field.placeholder || ""}
|
||||||
onChange={(value) => updateField(index, { placeholder: value })}
|
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -171,7 +169,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
|
|
||||||
{(!formScreen.fields || formScreen.fields.length === 0) && (
|
{(!formScreen.fields || formScreen.fields.length === 0) && (
|
||||||
<div className="text-center py-4 text-sm text-muted-foreground">
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
||||||
No fields added yet. Click "Add Field" to get started.
|
No fields added yet. Click "Add Field" to get started.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -182,9 +180,9 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Continue"
|
placeholder="Continue"
|
||||||
value={formScreen.bottomActionButton?.text || ""}
|
value={formScreen.bottomActionButton?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
bottomActionButton: {
|
bottomActionButton: {
|
||||||
text: value || "Continue",
|
text: e.target.value || "Continue",
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||||
import type { InfoScreenDefinition } from "@/lib/funnel/types";
|
import type { InfoScreenDefinition, TypographyVariant } from "@/lib/funnel/types";
|
||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
|
|
||||||
interface InfoScreenConfigProps {
|
interface InfoScreenConfigProps {
|
||||||
@ -12,7 +10,7 @@ interface InfoScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||||
const infoScreen = screen as InfoScreenDefinition & { position: any };
|
const infoScreen = screen as InfoScreenDefinition & { position: { x: number; y: number } };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -22,10 +20,10 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter screen title"
|
placeholder="Enter screen title"
|
||||||
value={infoScreen.title?.text || ""}
|
value={infoScreen.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
title: {
|
title: {
|
||||||
...infoScreen.title,
|
...infoScreen.title,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: infoScreen.title?.font || "manrope",
|
font: infoScreen.title?.font || "manrope",
|
||||||
weight: infoScreen.title?.weight || "bold",
|
weight: infoScreen.title?.weight || "bold",
|
||||||
align: infoScreen.title?.align || "center",
|
align: infoScreen.title?.align || "center",
|
||||||
@ -41,7 +39,7 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
title: {
|
title: {
|
||||||
...infoScreen.title,
|
...infoScreen.title,
|
||||||
text: infoScreen.title?.text || "",
|
text: infoScreen.title?.text || "",
|
||||||
font: e.target.value as any,
|
font: e.target.value as TypographyVariant['font'],
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -56,7 +54,7 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
title: {
|
title: {
|
||||||
...infoScreen.title,
|
...infoScreen.title,
|
||||||
text: infoScreen.title?.text || "",
|
text: infoScreen.title?.text || "",
|
||||||
weight: e.target.value as any,
|
weight: e.target.value as TypographyVariant['weight'],
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -73,9 +71,9 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter screen description"
|
placeholder="Enter screen description"
|
||||||
value={infoScreen.description?.text || ""}
|
value={infoScreen.description?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
description: value ? {
|
description: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: infoScreen.description?.font || "inter",
|
font: infoScreen.description?.font || "inter",
|
||||||
weight: infoScreen.description?.weight || "medium",
|
weight: infoScreen.description?.weight || "medium",
|
||||||
align: infoScreen.description?.align || "center",
|
align: infoScreen.description?.align || "center",
|
||||||
@ -112,11 +110,11 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
onChange={(e) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
icon: infoScreen.icon ? {
|
icon: infoScreen.icon ? {
|
||||||
...infoScreen.icon,
|
...infoScreen.icon,
|
||||||
size: e.target.value as any,
|
size: e.target.value as "sm" | "md" | "lg" | "xl",
|
||||||
} : {
|
} : {
|
||||||
type: "emoji",
|
type: "emoji",
|
||||||
value: "❤️",
|
value: "❤️",
|
||||||
size: e.target.value as any,
|
size: e.target.value as "sm" | "md" | "lg" | "xl",
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -130,10 +128,10 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder={infoScreen.icon?.type === "image" ? "Image URL" : "Emoji (e.g., ❤️)"}
|
placeholder={infoScreen.icon?.type === "image" ? "Image URL" : "Emoji (e.g., ❤️)"}
|
||||||
value={infoScreen.icon?.value || ""}
|
value={infoScreen.icon?.value || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
icon: value ? {
|
icon: e.target.value ? {
|
||||||
type: infoScreen.icon?.type || "emoji",
|
type: infoScreen.icon?.type || "emoji",
|
||||||
value,
|
value: e.target.value,
|
||||||
size: infoScreen.icon?.size || "lg",
|
size: infoScreen.icon?.size || "lg",
|
||||||
} : undefined
|
} : undefined
|
||||||
})}
|
})}
|
||||||
@ -146,9 +144,9 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Next"
|
placeholder="Next"
|
||||||
value={infoScreen.bottomActionButton?.text || ""}
|
value={infoScreen.bottomActionButton?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
bottomActionButton: value ? {
|
bottomActionButton: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
} : undefined
|
} : undefined
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { FormScreenConfig } from "./FormScreenConfig";
|
|||||||
import { TextScreenConfig } from "./TextScreenConfig";
|
import { TextScreenConfig } from "./TextScreenConfig";
|
||||||
|
|
||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
import type { ScreenDefinition } from "@/lib/funnel/types";
|
import type { ScreenDefinition, InfoScreenDefinition, DateScreenDefinition, CouponScreenDefinition, FormScreenDefinition, TextScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
interface TemplateConfigProps {
|
interface TemplateConfigProps {
|
||||||
screen: BuilderScreen;
|
screen: BuilderScreen;
|
||||||
@ -21,40 +21,40 @@ export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) {
|
|||||||
case "info":
|
case "info":
|
||||||
return (
|
return (
|
||||||
<InfoScreenConfig
|
<InfoScreenConfig
|
||||||
screen={screen as any}
|
screen={screen as BuilderScreen & { template: "info" }}
|
||||||
onUpdate={onUpdate as any}
|
onUpdate={onUpdate as (updates: Partial<InfoScreenDefinition>) => void}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "date":
|
case "date":
|
||||||
return (
|
return (
|
||||||
<DateScreenConfig
|
<DateScreenConfig
|
||||||
screen={screen as any}
|
screen={screen as BuilderScreen & { template: "date" }}
|
||||||
onUpdate={onUpdate as any}
|
onUpdate={onUpdate as (updates: Partial<DateScreenDefinition>) => void}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "coupon":
|
case "coupon":
|
||||||
return (
|
return (
|
||||||
<CouponScreenConfig
|
<CouponScreenConfig
|
||||||
screen={screen as any}
|
screen={screen as BuilderScreen & { template: "coupon" }}
|
||||||
onUpdate={onUpdate as any}
|
onUpdate={onUpdate as (updates: Partial<CouponScreenDefinition>) => void}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "form":
|
case "form":
|
||||||
return (
|
return (
|
||||||
<FormScreenConfig
|
<FormScreenConfig
|
||||||
screen={screen as any}
|
screen={screen as BuilderScreen & { template: "form" }}
|
||||||
onUpdate={onUpdate as any}
|
onUpdate={onUpdate as (updates: Partial<FormScreenDefinition>) => void}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
<TextScreenConfig
|
<TextScreenConfig
|
||||||
screen={screen as any}
|
screen={screen as BuilderScreen & { template: "text" }}
|
||||||
onUpdate={onUpdate as any}
|
onUpdate={onUpdate as (updates: Partial<TextScreenDefinition>) => void}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interface TextScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TextScreenConfig({ screen, onUpdate }: 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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -20,10 +20,10 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter screen title"
|
placeholder="Enter screen title"
|
||||||
value={textScreen.title?.text || ""}
|
value={textScreen.title?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
title: {
|
title: {
|
||||||
...textScreen.title,
|
...textScreen.title,
|
||||||
text: value,
|
text: e.target.value,
|
||||||
font: textScreen.title?.font || "manrope",
|
font: textScreen.title?.font || "manrope",
|
||||||
weight: textScreen.title?.weight || "bold",
|
weight: textScreen.title?.weight || "bold",
|
||||||
align: textScreen.title?.align || "center",
|
align: textScreen.title?.align || "center",
|
||||||
@ -39,7 +39,7 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
|
|||||||
title: {
|
title: {
|
||||||
...textScreen.title,
|
...textScreen.title,
|
||||||
text: textScreen.title?.text || "",
|
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: {
|
title: {
|
||||||
...textScreen.title,
|
...textScreen.title,
|
||||||
text: textScreen.title?.text || "",
|
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: {
|
title: {
|
||||||
...textScreen.title,
|
...textScreen.title,
|
||||||
text: textScreen.title?.text || "",
|
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: {
|
content: {
|
||||||
...textScreen.content,
|
...textScreen.content,
|
||||||
text: textScreen.content?.text || "",
|
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: {
|
content: {
|
||||||
...textScreen.content,
|
...textScreen.content,
|
||||||
text: textScreen.content?.text || "",
|
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: {
|
content: {
|
||||||
...textScreen.content,
|
...textScreen.content,
|
||||||
text: textScreen.content?.text || "",
|
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: {
|
content: {
|
||||||
...textScreen.content,
|
...textScreen.content,
|
||||||
text: textScreen.content?.text || "",
|
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) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Next"
|
placeholder="Next"
|
||||||
value={textScreen.bottomActionButton?.text || ""}
|
value={textScreen.bottomActionButton?.text || ""}
|
||||||
onChange={(value) => onUpdate({
|
onChange={(e) => onUpdate({
|
||||||
bottomActionButton: value ? {
|
bottomActionButton: e.target.value ? {
|
||||||
text: value,
|
text: e.target.value,
|
||||||
} : undefined
|
} : undefined
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -181,23 +181,6 @@ function getScreenById(funnel: FunnelDefinition, screenId: string) {
|
|||||||
return funnel.screens.find((screen) => screen.id === screenId);
|
return funnel.screens.find((screen) => screen.id === screenId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateScreenProgress(
|
|
||||||
currentScreenId: string,
|
|
||||||
funnel: FunnelDefinition,
|
|
||||||
answers: Record<string, string[]>
|
|
||||||
): { 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 {
|
function getCurrentTemplateRenderer(screen: ScreenDefinition): TemplateRenderer {
|
||||||
const renderer = TEMPLATE_REGISTRY[screen.template];
|
const renderer = TEMPLATE_REGISTRY[screen.template];
|
||||||
@ -221,8 +204,11 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
|
|
||||||
// Calculate automatic progress
|
// Calculate automatic progress
|
||||||
const screenProgress = useMemo(() => {
|
const screenProgress = useMemo(() => {
|
||||||
return calculateScreenProgress(currentScreen.id, funnel, answers);
|
const total = funnel.screens.length;
|
||||||
}, [currentScreen.id, funnel, answers]);
|
const currentIndex = funnel.screens.findIndex(screen => screen.id === currentScreen.id);
|
||||||
|
const current = currentIndex >= 0 ? currentIndex + 1 : 1;
|
||||||
|
return { current, total };
|
||||||
|
}, [currentScreen.id, funnel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerScreen(currentScreen.id);
|
registerScreen(currentScreen.id);
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import type { BottomActionButtonProps } from "@/components/widgets/BottomActionB
|
|||||||
import Typography from "@/components/ui/Typography/Typography";
|
import Typography from "@/components/ui/Typography/Typography";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildLayoutQuestionProps,
|
|
||||||
buildHeaderProgress,
|
buildHeaderProgress,
|
||||||
buildTypographyProps,
|
buildTypographyProps,
|
||||||
shouldShowBackButton,
|
shouldShowBackButton,
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import { createContext, useContext, useMemo, useReducer, type ReactNode } from "
|
|||||||
import type {
|
import type {
|
||||||
BuilderFunnelState,
|
BuilderFunnelState,
|
||||||
BuilderScreen,
|
BuilderScreen,
|
||||||
|
BuilderScreenPosition,
|
||||||
} from "@/lib/admin/builder/types";
|
} from "@/lib/admin/builder/types";
|
||||||
|
import type { ListScreenDefinition } from "@/lib/funnel/types";
|
||||||
import type { NavigationRuleDefinition } from "@/lib/funnel/types";
|
import type { NavigationRuleDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
interface BuilderState extends BuilderFunnelState {
|
interface BuilderState extends BuilderFunnelState {
|
||||||
@ -121,7 +123,7 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
|
|||||||
}
|
}
|
||||||
case "add-screen": {
|
case "add-screen": {
|
||||||
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
||||||
const newScreen: BuilderScreen = {
|
const baseScreen = {
|
||||||
...INITIAL_SCREEN,
|
...INITIAL_SCREEN,
|
||||||
id: nextId,
|
id: nextId,
|
||||||
position: {
|
position: {
|
||||||
@ -129,23 +131,27 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
|
|||||||
y: (action.payload?.position?.y ?? 120) + state.screens.length * 20,
|
y: (action.payload?.position?.y ?? 120) + state.screens.length * 20,
|
||||||
},
|
},
|
||||||
...action.payload,
|
...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: {
|
navigation: {
|
||||||
defaultNextScreenId: action.payload?.navigation?.defaultNextScreenId,
|
defaultNextScreenId: action.payload?.navigation?.defaultNextScreenId,
|
||||||
rules: action.payload?.navigation?.rules ?? [],
|
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, {
|
return withDirty(state, {
|
||||||
...state,
|
...state,
|
||||||
screens: [...state.screens, newScreen],
|
screens: [...state.screens, newScreen],
|
||||||
@ -182,22 +188,23 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
|
|||||||
...state,
|
...state,
|
||||||
screens: state.screens.map((current) =>
|
screens: state.screens.map((current) =>
|
||||||
current.id === screenId
|
current.id === screenId
|
||||||
? {
|
? ({
|
||||||
...current,
|
...current,
|
||||||
...screen,
|
...screen,
|
||||||
title: screen.title ? { ...current.title, ...screen.title } : current.title,
|
title: screen.title ? { ...current.title, ...screen.title } : current.title,
|
||||||
subtitle:
|
...(('subtitle' in screen && screen.subtitle !== undefined) ? {
|
||||||
screen.subtitle !== undefined
|
subtitle: screen.subtitle
|
||||||
? screen.subtitle
|
} : ('subtitle' in current) ? {
|
||||||
: current.subtitle,
|
subtitle: current.subtitle
|
||||||
list: screen.list
|
} : {}),
|
||||||
? {
|
...(current.template === "list" && 'list' in screen && screen.list ? {
|
||||||
...current.list,
|
list: {
|
||||||
...screen.list,
|
...(current as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
||||||
options: screen.list.options ?? current.list.options,
|
...screen.list,
|
||||||
}
|
options: screen.list.options ?? (current as ListScreenDefinition & { position: BuilderScreenPosition }).list.options,
|
||||||
: current.list,
|
}
|
||||||
}
|
} : {}),
|
||||||
|
} as BuilderScreen)
|
||||||
: current
|
: current
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,9 +24,9 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
|
|||||||
id: "list",
|
id: "list",
|
||||||
label: "Вопрос с вариантами",
|
label: "Вопрос с вариантами",
|
||||||
create: ({ screenId, position }, overrides) => {
|
create: ({ screenId, position }, overrides) => {
|
||||||
const base: BuilderScreen = {
|
const base = {
|
||||||
id: screenId,
|
id: screenId,
|
||||||
template: "list",
|
template: "list" as const,
|
||||||
header: {
|
header: {
|
||||||
progress: {
|
progress: {
|
||||||
current: 1,
|
current: 1,
|
||||||
@ -36,16 +36,16 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
text: "Новый экран",
|
text: "Новый экран",
|
||||||
font: "manrope",
|
font: "manrope" as const,
|
||||||
weight: "bold",
|
weight: "bold" as const,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
text: "Опишите вопрос справа",
|
text: "Опишите вопрос справа",
|
||||||
color: "muted",
|
color: "muted" as const,
|
||||||
font: "inter",
|
font: "inter" as const,
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
selectionType: "single",
|
selectionType: "single" as const,
|
||||||
options: cloneOptions([
|
options: cloneOptions([
|
||||||
{ id: "option-1", label: "Вариант 1" },
|
{ id: "option-1", label: "Вариант 1" },
|
||||||
{ id: "option-2", label: "Вариант 2" },
|
{ id: "option-2", label: "Вариант 2" },
|
||||||
@ -59,13 +59,13 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!overrides) {
|
if (!overrides) {
|
||||||
return base;
|
return base as BuilderScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...overrides,
|
...overrides,
|
||||||
list: overrides.list
|
list: ('list' in overrides && overrides.list)
|
||||||
? {
|
? {
|
||||||
...base.list,
|
...base.list,
|
||||||
...overrides.list,
|
...overrides.list,
|
||||||
@ -79,7 +79,7 @@ const LIST_TEMPLATE: BuilderTemplateDefinition = {
|
|||||||
rules: overrides.navigation.rules ?? base.navigation?.rules ?? [],
|
rules: overrides.navigation.rules ?? base.navigation?.rules ?? [],
|
||||||
}
|
}
|
||||||
: base.navigation,
|
: base.navigation,
|
||||||
};
|
} as BuilderScreen;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import type { BuilderState } from "@/lib/admin/builder/context";
|
import type { BuilderState } from "@/lib/admin/builder/context";
|
||||||
import type { BuilderScreen, BuilderFunnelState } from "@/lib/admin/builder/types";
|
import type { BuilderScreen, BuilderFunnelState, BuilderScreenPosition } from "@/lib/admin/builder/types";
|
||||||
import type { FunnelDefinition, ListScreenDefinition } from "@/lib/funnel/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) => ({
|
return screens.map((screen, index) => ({
|
||||||
...screen,
|
...screen,
|
||||||
position: {
|
position: {
|
||||||
x: 120 + (index % 4) * 240,
|
x: 120 + (index % 4) * 240,
|
||||||
y: 120 + Math.floor(index / 4) * 200,
|
y: 120 + Math.floor(index / 4) * 200,
|
||||||
},
|
},
|
||||||
}));
|
})) as BuilderScreen[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderState {
|
export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderState {
|
||||||
@ -24,7 +24,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
|
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"] = {
|
const meta: FunnelDefinition["meta"] = {
|
||||||
...state.meta,
|
...state.meta,
|
||||||
firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id,
|
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>): BuilderScreen {
|
export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderScreen>): BuilderScreen {
|
||||||
const copy: BuilderScreen = {
|
const copy = {
|
||||||
...screen,
|
...screen,
|
||||||
position: { ...screen.position },
|
position: { ...screen.position },
|
||||||
list: {
|
...(screen.template === "list" && 'list' in screen ? {
|
||||||
...screen.list,
|
list: {
|
||||||
options: screen.list.options.map((option) => ({ ...option })),
|
...(screen as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
||||||
},
|
options: (screen as ListScreenDefinition & { position: BuilderScreenPosition }).list.options.map((option) => ({ ...option })),
|
||||||
|
}
|
||||||
|
} : {}),
|
||||||
navigation: screen.navigation
|
navigation: screen.navigation
|
||||||
? {
|
? {
|
||||||
defaultNextScreenId: screen.navigation.defaultNextScreenId,
|
defaultNextScreenId: screen.navigation.defaultNextScreenId,
|
||||||
@ -57,12 +59,12 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderSc
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
} as BuilderScreen;
|
||||||
|
|
||||||
return overrides ? { ...copy, ...overrides } : copy;
|
return overrides ? { ...copy, ...overrides } as BuilderScreen : copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBuilderFunnelState(state: BuilderState): BuilderFunnelState {
|
export function toBuilderFunnelState(state: BuilderState): BuilderFunnelState {
|
||||||
const { isDirty, selectedScreenId, ...rest } = state;
|
const { isDirty: _isDirty, selectedScreenId: _selectedScreenId, ...rest } = state;
|
||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,8 +45,8 @@ function validateOptionIds(screen: BuilderScreen, issues: BuilderValidationIssue
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenWithList = screen as any;
|
const screenWithList = screen as { list: { options: { id: string }[] } };
|
||||||
const duplicates = collectDuplicateIds(screenWithList.list.options.map((option: any) => option.id));
|
const duplicates = collectDuplicateIds(screenWithList.list.options.map((option) => option.id));
|
||||||
for (const duplicateId of duplicates) {
|
for (const duplicateId of duplicates) {
|
||||||
issues.push(
|
issues.push(
|
||||||
createIssue(
|
createIssue(
|
||||||
@ -136,8 +136,8 @@ function validateNavigation(screen: BuilderScreen, state: BuilderState, issues:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceScreenWithList = referenceScreen as any;
|
const referenceScreenWithList = referenceScreen as { list: { options: { id: string }[] } };
|
||||||
const availableOptionIds = new Set(referenceScreenWithList.list.options.map((option: any) => option.id));
|
const availableOptionIds = new Set(referenceScreenWithList.list.options.map((option) => option.id));
|
||||||
const missingOptionIds = (condition.optionIds ?? []).filter((optionId) => !availableOptionIds.has(optionId));
|
const missingOptionIds = (condition.optionIds ?? []).filter((optionId) => !availableOptionIds.has(optionId));
|
||||||
if (missingOptionIds.length > 0) {
|
if (missingOptionIds.length > 0) {
|
||||||
issues.push(
|
issues.push(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user