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 { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
||||
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
||||
import { BuilderPreview } from "@/components/admin/builder/BuilderPreview";
|
||||
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
||||
import {
|
||||
BuilderProvider,
|
||||
useBuilderDispatch,
|
||||
useBuilderState,
|
||||
} 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() {
|
||||
const dispatch = useBuilderDispatch();
|
||||
const state = useBuilderState();
|
||||
const [exportJson, setExportJson] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPreview, setShowPreview] = useState<boolean>(true);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
dispatch({ type: "reset" });
|
||||
}, [dispatch]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
const json = JSON.stringify(serializeBuilderState(state), null, 2);
|
||||
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(() => {
|
||||
setShowPreview((prev: boolean) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleTogglePreview = useCallback(() => {
|
||||
setShowPreview(prev => !prev);
|
||||
const handleLoadError = useCallback((message: string) => {
|
||||
console.error("Load error:", message);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -93,26 +41,18 @@ function BuilderView() {
|
||||
}
|
||||
sidebar={<BuilderSidebar />}
|
||||
canvas={<BuilderCanvas />}
|
||||
preview={<BuilderPreview />}
|
||||
showPreview={showPreview}
|
||||
onTogglePreview={handleTogglePreview}
|
||||
/>
|
||||
|
||||
{exportJson && (
|
||||
<ExportModal
|
||||
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="fixed bottom-6 right-6 z-50 max-w-sm rounded-xl border border-border bg-background p-4 shadow-lg">
|
||||
<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
|
||||
type="button"
|
||||
className="text-xs text-destructive underline"
|
||||
onClick={() => setError(null)}
|
||||
className="text-xs text-muted-foreground underline"
|
||||
onClick={() => setExportJson(null)}
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
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";
|
||||
|
||||
const CARD_WIDTH = 280;
|
||||
@ -37,7 +37,7 @@ export function BuilderCanvas() {
|
||||
e.preventDefault();
|
||||
if (!dragStateRef.current) return;
|
||||
|
||||
const { screenId, dragStartIndex, currentIndex } = dragStateRef.current;
|
||||
const { dragStartIndex, currentIndex } = dragStateRef.current;
|
||||
|
||||
if (dragStartIndex !== currentIndex) {
|
||||
dispatch({
|
||||
@ -63,39 +63,16 @@ export function BuilderCanvas() {
|
||||
dispatch({ type: "add-screen" });
|
||||
}, [dispatch]);
|
||||
|
||||
const renderArrows = () => {
|
||||
const arrows: JSX.Element[] = [];
|
||||
|
||||
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;
|
||||
// Helper functions for type checking
|
||||
const hasSubtitle = (screen: ScreenDefinition): screen is ScreenDefinition & { subtitle: { text: string } } => {
|
||||
return 'subtitle' in screen && screen.subtitle !== undefined;
|
||||
};
|
||||
|
||||
const isListScreen = (screen: ScreenDefinition): screen is ListScreenDefinition => {
|
||||
return screen.template === 'list';
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="h-full w-full overflow-auto bg-slate-50 dark:bg-slate-900">
|
||||
{/* Header with Add Button */}
|
||||
@ -153,30 +130,30 @@ export function BuilderCanvas() {
|
||||
<h3 className="text-base font-semibold leading-5 text-foreground mb-2">
|
||||
{screen.title.text || "Без названия"}
|
||||
</h3>
|
||||
{(screen as any).subtitle?.text && (
|
||||
<p className="text-xs text-muted-foreground mb-3">{(screen as any).subtitle.text}</p>
|
||||
{hasSubtitle(screen) && (
|
||||
<p className="text-xs text-muted-foreground mb-3">{screen.subtitle.text}</p>
|
||||
)}
|
||||
|
||||
{/* List Screen Details */}
|
||||
{(screen as any).list && (
|
||||
{isListScreen(screen) && (
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Тип выбора:</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{(screen as any).list.selectionType === "single" ? "Single" : "Multi"}
|
||||
{screen.list.selectionType === "single" ? "Single" : "Multi"}
|
||||
</span>
|
||||
</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">
|
||||
{(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]">
|
||||
{option.label}
|
||||
</span>
|
||||
))}
|
||||
{(screen as any).list.options.length > 2 && (
|
||||
{screen.list.options.length > 2 && (
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
+{(screen as any).list.options.length - 2} ещё
|
||||
+{screen.list.options.length - 2} ещё
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,18 +9,17 @@ import { FormTemplate } from "@/components/funnel/templates/FormTemplate";
|
||||
import { TextTemplate } from "@/components/funnel/templates/TextTemplate";
|
||||
import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate";
|
||||
import { useBuilderSelectedScreen } from "@/lib/admin/builder/context";
|
||||
import type { ListScreenDefinition, InfoScreenDefinition, DateScreenDefinition, FormScreenDefinition, TextScreenDefinition, CouponScreenDefinition } from "@/lib/funnel/types";
|
||||
|
||||
export function BuilderPreview() {
|
||||
const selectedScreen = useBuilderSelectedScreen();
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
const [dateData, setDateData] = useState<[number, number, number]>([0, 0, 0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedScreen) {
|
||||
setSelectedIds([]);
|
||||
setFormData({});
|
||||
setDateData([0, 0, 0]);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -45,9 +44,6 @@ export function BuilderPreview() {
|
||||
setFormData(data);
|
||||
}, []);
|
||||
|
||||
const handleDateChange = useCallback((data: [number, number, number]) => {
|
||||
setDateData(data);
|
||||
}, []);
|
||||
|
||||
const renderScreenPreview = useCallback(() => {
|
||||
if (!selectedScreen) return null;
|
||||
@ -64,7 +60,7 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<ListTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as ListScreenDefinition}
|
||||
selectedOptionIds={selectedIds}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
@ -74,7 +70,7 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<InfoTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as InfoScreenDefinition}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -82,7 +78,7 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<DateTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as DateScreenDefinition}
|
||||
selectedDate={{ month: "", day: "", year: "" }}
|
||||
onDateChange={() => {}}
|
||||
/>
|
||||
@ -92,7 +88,7 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<FormTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as FormScreenDefinition}
|
||||
formData={formData}
|
||||
onFormDataChange={handleFormChange}
|
||||
/>
|
||||
@ -102,7 +98,7 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<TextTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as TextScreenDefinition}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -110,14 +106,14 @@ export function BuilderPreview() {
|
||||
return (
|
||||
<CouponTemplate
|
||||
{...commonProps}
|
||||
screen={selectedScreen as any}
|
||||
screen={selectedScreen as CouponScreenDefinition}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,6 +10,15 @@ import type { NavigationRuleDefinition } from "@/lib/funnel/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
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({
|
||||
title,
|
||||
description,
|
||||
@ -30,9 +39,6 @@ function Section({
|
||||
);
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return <div className="h-px w-full bg-border/80" />;
|
||||
}
|
||||
|
||||
function ValidationSummary() {
|
||||
const state = useBuilderState();
|
||||
@ -92,9 +98,13 @@ export function BuilderSidebar() {
|
||||
|
||||
const updateList = (
|
||||
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,
|
||||
...listUpdates,
|
||||
selectionType: listUpdates.selectionType ?? screen.list.selectionType,
|
||||
@ -131,10 +141,10 @@ export function BuilderSidebar() {
|
||||
|
||||
const handleSelectionTypeChange = (
|
||||
screenId: string,
|
||||
selectionType: BuilderScreen["list"]["selectionType"]
|
||||
selectionType: "single" | "multi"
|
||||
) => {
|
||||
const screen = getScreenById(screenId);
|
||||
if (!screen) {
|
||||
if (!screen || !isListScreen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -176,7 +186,7 @@ export function BuilderSidebar() {
|
||||
value: string
|
||||
) => {
|
||||
const screen = getScreenById(screenId);
|
||||
if (!screen) {
|
||||
if (!screen || !isListScreen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -188,6 +198,10 @@ export function BuilderSidebar() {
|
||||
};
|
||||
|
||||
const handleAddOption = (screen: BuilderScreen) => {
|
||||
if (!isListScreen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = screen.list.options.length + 1;
|
||||
const options = [
|
||||
...screen.list.options,
|
||||
@ -201,6 +215,10 @@ export function BuilderSidebar() {
|
||||
};
|
||||
|
||||
const handleRemoveOption = (screen: BuilderScreen, index: number) => {
|
||||
if (!isListScreen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = screen.list.options.filter((_, optionIndex) => optionIndex !== index);
|
||||
updateList(screen, { options });
|
||||
};
|
||||
@ -304,6 +322,10 @@ export function BuilderSidebar() {
|
||||
};
|
||||
|
||||
const handleAddRule = (screen: BuilderScreen) => {
|
||||
if (!isListScreen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultCondition: NavigationRuleDefinition["conditions"][number] = {
|
||||
screenId: screen.id,
|
||||
operator: "includesAny",
|
||||
@ -389,7 +411,7 @@ export function BuilderSidebar() {
|
||||
}
|
||||
|
||||
// Показываем настройки выбранного экрана
|
||||
const isListScreen = selectedScreen.template === "list" && "list" in selectedScreen;
|
||||
const selectedScreenIsListType = isListScreen(selectedScreen);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@ -425,7 +447,7 @@ export function BuilderSidebar() {
|
||||
/>
|
||||
<TextInput
|
||||
label="Подзаголовок"
|
||||
value={selectedScreen.subtitle?.text ?? ""}
|
||||
value={hasSubtitle(selectedScreen) ? selectedScreen.subtitle?.text ?? "" : ""}
|
||||
onChange={(event) => handleSubtitleChange(selectedScreen.id, event.target.value)}
|
||||
/>
|
||||
<div className="rounded-lg border border-border/60 bg-muted/30 p-3">
|
||||
@ -439,7 +461,7 @@ export function BuilderSidebar() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{isListScreen && (
|
||||
{selectedScreenIsListType && (
|
||||
<Section title="Варианты ответа" description="Настройки опций">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -447,11 +469,11 @@ export function BuilderSidebar() {
|
||||
<span className="text-sm font-medium text-muted-foreground">Тип выбора</span>
|
||||
<select
|
||||
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) =>
|
||||
handleSelectionTypeChange(
|
||||
selectedScreen.id,
|
||||
event.target.value as any
|
||||
event.target.value as "single" | "multi"
|
||||
)
|
||||
}
|
||||
>
|
||||
@ -461,13 +483,13 @@ export function BuilderSidebar() {
|
||||
</label>
|
||||
<Button
|
||||
className="h-8 px-3 text-xs"
|
||||
onClick={() => handleAddOption(selectedScreen as any)}
|
||||
onClick={() => handleAddOption(selectedScreen)}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
<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
|
||||
key={option.id}
|
||||
className={cn(
|
||||
@ -477,11 +499,11 @@ export function BuilderSidebar() {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<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
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
onClick={() => handleRemoveOption(selectedScreen as any, index)}
|
||||
onClick={() => handleRemoveOption(selectedScreen, index)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
@ -536,14 +558,14 @@ export function BuilderSidebar() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{isListScreen && (
|
||||
{selectedScreenIsListType && (
|
||||
<Section title="Правила переходов" description="Условная навигация">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Направляйте пользователей на разные экраны в зависимости от выбора.
|
||||
</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>
|
||||
</div>
|
||||
@ -591,7 +613,7 @@ export function BuilderSidebar() {
|
||||
<div className="flex flex-col gap-2">
|
||||
<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">
|
||||
{(selectedScreen as any).list?.options?.map((option: any) => {
|
||||
{selectedScreenIsListType && selectedScreen.list.options.map((option) => {
|
||||
const condition = rule.conditions[0];
|
||||
const isChecked = condition.optionIds?.includes(option.id) ?? false;
|
||||
return (
|
||||
|
||||
@ -10,7 +10,7 @@ interface 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 (
|
||||
<div className="space-y-4">
|
||||
@ -20,10 +20,10 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
||||
<TextInput
|
||||
placeholder="You're Lucky!"
|
||||
value={couponScreen.title?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
title: {
|
||||
...couponScreen.title,
|
||||
text: value,
|
||||
text: e.target.value,
|
||||
font: couponScreen.title?.font || "manrope",
|
||||
weight: couponScreen.title?.weight || "bold",
|
||||
align: couponScreen.title?.align || "center",
|
||||
@ -38,10 +38,10 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
||||
<TextInput
|
||||
placeholder="You got an exclusive 94% discount"
|
||||
value={couponScreen.subtitle?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
subtitle: {
|
||||
...couponScreen.subtitle,
|
||||
text: value,
|
||||
text: e.target.value,
|
||||
font: couponScreen.subtitle?.font || "inter",
|
||||
weight: couponScreen.subtitle?.weight || "medium",
|
||||
align: couponScreen.subtitle?.align || "center",
|
||||
@ -55,28 +55,44 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
||||
<h3 className="text-sm font-semibold">Coupon Details</h3>
|
||||
|
||||
<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
|
||||
placeholder="94% OFF"
|
||||
value={couponScreen.coupon?.discountTitle || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
value={couponScreen.coupon?.offer?.title?.text || ""}
|
||||
onChange={(e) => onUpdate({
|
||||
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 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
|
||||
placeholder="HAIR LOSS SPECIALIST"
|
||||
value={couponScreen.coupon?.discountDescription || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
value={couponScreen.coupon?.offer?.description?.text || ""}
|
||||
onChange={(e) => onUpdate({
|
||||
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>
|
||||
<TextInput
|
||||
placeholder="HAIR50"
|
||||
value={couponScreen.coupon?.promoCode || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
value={couponScreen.coupon?.promoCode?.text || ""}
|
||||
onChange={(e) => onUpdate({
|
||||
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>
|
||||
<TextInput
|
||||
placeholder="Click to copy promocode"
|
||||
value={couponScreen.coupon?.footerText || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
value={couponScreen.coupon?.footer?.text || ""}
|
||||
onChange={(e) => onUpdate({
|
||||
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
|
||||
placeholder="Continue"
|
||||
value={couponScreen.bottomActionButton?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
bottomActionButton: {
|
||||
text: value || "Continue",
|
||||
text: e.target.value || "Continue",
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -10,7 +10,7 @@ interface 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 (
|
||||
<div className="space-y-4">
|
||||
@ -20,10 +20,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="When were you born?"
|
||||
value={dateScreen.title?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
title: {
|
||||
...dateScreen.title,
|
||||
text: value,
|
||||
text: e.target.value,
|
||||
font: dateScreen.title?.font || "manrope",
|
||||
weight: dateScreen.title?.weight || "bold",
|
||||
}
|
||||
@ -37,9 +37,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Enter subtitle"
|
||||
value={dateScreen.subtitle?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
subtitle: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
subtitle: e.target.value ? {
|
||||
text: e.target.value,
|
||||
font: dateScreen.subtitle?.font || "inter",
|
||||
weight: dateScreen.subtitle?.weight || "medium",
|
||||
color: dateScreen.subtitle?.color || "muted",
|
||||
@ -58,10 +58,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Month"
|
||||
value={dateScreen.dateInput?.monthLabel || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
monthLabel: value,
|
||||
monthLabel: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -72,10 +72,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Day"
|
||||
value={dateScreen.dateInput?.dayLabel || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
dayLabel: value,
|
||||
dayLabel: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -86,10 +86,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Year"
|
||||
value={dateScreen.dateInput?.yearLabel || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
yearLabel: value,
|
||||
yearLabel: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -102,10 +102,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="MM"
|
||||
value={dateScreen.dateInput?.monthPlaceholder || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
monthPlaceholder: value,
|
||||
monthPlaceholder: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -116,10 +116,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="DD"
|
||||
value={dateScreen.dateInput?.dayPlaceholder || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
dayPlaceholder: value,
|
||||
dayPlaceholder: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -130,10 +130,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="YYYY"
|
||||
value={dateScreen.dateInput?.yearPlaceholder || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
dateInput: {
|
||||
...dateScreen.dateInput,
|
||||
yearPlaceholder: value,
|
||||
yearPlaceholder: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -147,9 +147,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="We protect your personal data"
|
||||
value={dateScreen.infoMessage?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
infoMessage: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
infoMessage: e.target.value ? {
|
||||
text: e.target.value,
|
||||
icon: dateScreen.infoMessage?.icon || "🔒",
|
||||
} : undefined
|
||||
})}
|
||||
@ -159,10 +159,10 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="🔒"
|
||||
value={dateScreen.infoMessage.icon}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
infoMessage: {
|
||||
text: dateScreen.infoMessage?.text || "",
|
||||
icon: value,
|
||||
icon: e.target.value,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@ -175,9 +175,9 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Next"
|
||||
value={dateScreen.bottomActionButton?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
bottomActionButton: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
bottomActionButton: e.target.value ? {
|
||||
text: e.target.value,
|
||||
} : undefined
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -11,7 +11,7 @@ interface 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 newFields = [...(formScreen.fields || [])];
|
||||
@ -46,10 +46,10 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Enter your details"
|
||||
value={formScreen.title?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
title: {
|
||||
...formScreen.title,
|
||||
text: value,
|
||||
text: e.target.value,
|
||||
font: formScreen.title?.font || "manrope",
|
||||
weight: formScreen.title?.weight || "bold",
|
||||
}
|
||||
@ -63,9 +63,9 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Please fill in all fields"
|
||||
value={formScreen.subtitle?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
subtitle: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
subtitle: e.target.value ? {
|
||||
text: e.target.value,
|
||||
font: formScreen.subtitle?.font || "inter",
|
||||
weight: formScreen.subtitle?.weight || "medium",
|
||||
color: formScreen.subtitle?.color || "muted",
|
||||
@ -79,7 +79,6 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Form Fields</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={addField}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
@ -92,7 +91,6 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Field {index + 1}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeField(index)}
|
||||
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
|
||||
placeholder="field_id"
|
||||
value={field.id}
|
||||
onChange={(value) => updateField(index, { id: value })}
|
||||
onChange={(e) => updateField(index, { id: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -116,7 +114,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<select
|
||||
className="w-full rounded border border-border bg-background px-2 py-1 text-sm"
|
||||
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="email">Email</option>
|
||||
@ -131,7 +129,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Field Label"
|
||||
value={field.label}
|
||||
onChange={(value) => updateField(index, { label: value })}
|
||||
onChange={(e) => updateField(index, { label: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -140,7 +138,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Enter placeholder"
|
||||
value={field.placeholder || ""}
|
||||
onChange={(value) => updateField(index, { placeholder: value })}
|
||||
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -171,7 +169,7 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
|
||||
{(!formScreen.fields || formScreen.fields.length === 0) && (
|
||||
<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>
|
||||
@ -182,9 +180,9 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Continue"
|
||||
value={formScreen.bottomActionButton?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
bottomActionButton: {
|
||||
text: value || "Continue",
|
||||
text: e.target.value || "Continue",
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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";
|
||||
|
||||
interface InfoScreenConfigProps {
|
||||
@ -12,7 +10,7 @@ interface 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 (
|
||||
<div className="space-y-4">
|
||||
@ -22,10 +20,10 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Enter screen title"
|
||||
value={infoScreen.title?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
onChange={(e) => onUpdate({
|
||||
title: {
|
||||
...infoScreen.title,
|
||||
text: value,
|
||||
text: e.target.value,
|
||||
font: infoScreen.title?.font || "manrope",
|
||||
weight: infoScreen.title?.weight || "bold",
|
||||
align: infoScreen.title?.align || "center",
|
||||
@ -41,7 +39,7 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
title: {
|
||||
...infoScreen.title,
|
||||
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: {
|
||||
...infoScreen.title,
|
||||
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
|
||||
placeholder="Enter screen description"
|
||||
value={infoScreen.description?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
description: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
description: e.target.value ? {
|
||||
text: e.target.value,
|
||||
font: infoScreen.description?.font || "inter",
|
||||
weight: infoScreen.description?.weight || "medium",
|
||||
align: infoScreen.description?.align || "center",
|
||||
@ -112,11 +110,11 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
onChange={(e) => onUpdate({
|
||||
icon: infoScreen.icon ? {
|
||||
...infoScreen.icon,
|
||||
size: e.target.value as any,
|
||||
size: e.target.value as "sm" | "md" | "lg" | "xl",
|
||||
} : {
|
||||
type: "emoji",
|
||||
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
|
||||
placeholder={infoScreen.icon?.type === "image" ? "Image URL" : "Emoji (e.g., ❤️)"}
|
||||
value={infoScreen.icon?.value || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
icon: value ? {
|
||||
onChange={(e) => onUpdate({
|
||||
icon: e.target.value ? {
|
||||
type: infoScreen.icon?.type || "emoji",
|
||||
value,
|
||||
value: e.target.value,
|
||||
size: infoScreen.icon?.size || "lg",
|
||||
} : undefined
|
||||
})}
|
||||
@ -146,9 +144,9 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Next"
|
||||
value={infoScreen.bottomActionButton?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
bottomActionButton: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
bottomActionButton: e.target.value ? {
|
||||
text: e.target.value,
|
||||
} : undefined
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -7,7 +7,7 @@ import { FormScreenConfig } from "./FormScreenConfig";
|
||||
import { TextScreenConfig } from "./TextScreenConfig";
|
||||
|
||||
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 {
|
||||
screen: BuilderScreen;
|
||||
@ -21,40 +21,40 @@ export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) {
|
||||
case "info":
|
||||
return (
|
||||
<InfoScreenConfig
|
||||
screen={screen as any}
|
||||
onUpdate={onUpdate as any}
|
||||
screen={screen as BuilderScreen & { template: "info" }}
|
||||
onUpdate={onUpdate as (updates: Partial<InfoScreenDefinition>) => void}
|
||||
/>
|
||||
);
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<DateScreenConfig
|
||||
screen={screen as any}
|
||||
onUpdate={onUpdate as any}
|
||||
screen={screen as BuilderScreen & { template: "date" }}
|
||||
onUpdate={onUpdate as (updates: Partial<DateScreenDefinition>) => void}
|
||||
/>
|
||||
);
|
||||
|
||||
case "coupon":
|
||||
return (
|
||||
<CouponScreenConfig
|
||||
screen={screen as any}
|
||||
onUpdate={onUpdate as any}
|
||||
screen={screen as BuilderScreen & { template: "coupon" }}
|
||||
onUpdate={onUpdate as (updates: Partial<CouponScreenDefinition>) => void}
|
||||
/>
|
||||
);
|
||||
|
||||
case "form":
|
||||
return (
|
||||
<FormScreenConfig
|
||||
screen={screen as any}
|
||||
onUpdate={onUpdate as any}
|
||||
screen={screen as BuilderScreen & { template: "form" }}
|
||||
onUpdate={onUpdate as (updates: Partial<FormScreenDefinition>) => void}
|
||||
/>
|
||||
);
|
||||
|
||||
case "text":
|
||||
return (
|
||||
<TextScreenConfig
|
||||
screen={screen as any}
|
||||
onUpdate={onUpdate as any}
|
||||
screen={screen as BuilderScreen & { template: "text" }}
|
||||
onUpdate={onUpdate as (updates: Partial<TextScreenDefinition>) => void}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="space-y-4">
|
||||
@ -20,10 +20,10 @@ export function TextScreenConfig({ screen, onUpdate }: TextScreenConfigProps) {
|
||||
<TextInput
|
||||
placeholder="Enter screen title"
|
||||
value={textScreen.title?.text || ""}
|
||||
onChange={(value) => 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) {
|
||||
<TextInput
|
||||
placeholder="Next"
|
||||
value={textScreen.bottomActionButton?.text || ""}
|
||||
onChange={(value) => onUpdate({
|
||||
bottomActionButton: value ? {
|
||||
text: value,
|
||||
onChange={(e) => onUpdate({
|
||||
bottomActionButton: e.target.value ? {
|
||||
text: e.target.value,
|
||||
} : undefined
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -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<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 {
|
||||
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);
|
||||
|
||||
@ -10,7 +10,6 @@ import type { BottomActionButtonProps } from "@/components/widgets/BottomActionB
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
import {
|
||||
buildLayoutQuestionProps,
|
||||
buildHeaderProgress,
|
||||
buildTypographyProps,
|
||||
shouldShowBackButton,
|
||||
|
||||
@ -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
|
||||
),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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>): 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<BuilderSc
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
} as BuilderScreen;
|
||||
|
||||
return overrides ? { ...copy, ...overrides } : copy;
|
||||
return overrides ? { ...copy, ...overrides } as BuilderScreen : copy;
|
||||
}
|
||||
|
||||
export function toBuilderFunnelState(state: BuilderState): BuilderFunnelState {
|
||||
const { isDirty, selectedScreenId, ...rest } = state;
|
||||
const { isDirty: _isDirty, selectedScreenId: _selectedScreenId, ...rest } = state;
|
||||
return rest;
|
||||
}
|
||||
|
||||
@ -45,8 +45,8 @@ function validateOptionIds(screen: BuilderScreen, issues: BuilderValidationIssue
|
||||
return;
|
||||
}
|
||||
|
||||
const screenWithList = screen as any;
|
||||
const duplicates = collectDuplicateIds(screenWithList.list.options.map((option: any) => 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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user