w-funnel/src/components/admin/builder/forms/variants/VariantConditionEditor.tsx
dev.daminik00 6c50d05123 ab
2025-10-21 01:27:08 +02:00

285 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo } from "react";
import { AgeSelector } from "../AgeSelector";
import { ZodiacSelector } from "../ZodiacSelector";
import { EmailDomainSelector } from "../EmailDomainSelector";
import { ConditionTypeSelector } from "../ConditionTypeSelector";
import { UnleashConditionEditor } from "../UnleashConditionEditor";
import type { VariantConditionEditorProps } from "./types";
import type { BuilderScreen } from "@/lib/admin/builder/types";
import type { ListOptionDefinition } from "@/lib/funnel/types";
/**
* Редактор условия для варианта экрана
*/
export function VariantConditionEditor({
condition,
allScreens,
onChange,
}: VariantConditionEditorProps) {
// Находим выбранный экран (может быть ID экрана или storageKey для zodiac)
const selectedScreen = useMemo(() => {
// Сначала ищем по ID экрана
let screen = allScreens.find((s) => s.id === condition.screenId);
if (screen) return screen;
// Если не нашли, ищем date экран где storageKey === condition.screenId
screen = allScreens.find((s) => {
if (s.template === "date") {
const dateScreen = s as BuilderScreen & { template: "date"; dateInput?: { zodiac?: { enabled?: boolean; storageKey?: string } } };
const zodiacSettings = dateScreen.dateInput?.zodiac;
return zodiacSettings?.enabled && zodiacSettings.storageKey?.trim() === condition.screenId;
}
return false;
});
return screen;
}, [allScreens, condition.screenId]);
// Определяем опции для условия (если это list экран)
// Собираем опции из базового экрана + из всех вариантов
const conditionOptions = useMemo<ListOptionDefinition[]>(() => {
if (!selectedScreen || selectedScreen.template !== "list") {
return [];
}
const listScreen = selectedScreen as BuilderScreen & {
template: "list";
list: { options: ListOptionDefinition[] };
variants?: Array<{ overrides?: { list?: { options?: ListOptionDefinition[] } } }>;
};
// Начинаем с базовых опций
const optionsMap = new Map<string, ListOptionDefinition>();
listScreen.list.options.forEach(opt => {
optionsMap.set(opt.id, opt);
});
// Добавляем опции из всех вариантов
if (listScreen.variants) {
listScreen.variants.forEach(variant => {
const variantOptions = variant.overrides?.list?.options;
if (variantOptions) {
variantOptions.forEach(opt => {
// Проверяем что опция валидна (имеет id)
if (opt && opt.id) {
// Добавляем или переопределяем опцию
optionsMap.set(opt.id, opt as ListOptionDefinition);
}
});
}
});
}
// Возвращаем все уникальные опции
return Array.from(optionsMap.values());
}, [selectedScreen]);
// Определяем, нужен ли специальный селектор
const showZodiacSelector = useMemo(() => {
if (!selectedScreen) return false;
// Проверяем специальный zodiac экран
if (selectedScreen.id === "zodiac-sign" || selectedScreen.id === "zodiac") {
return true;
}
// Проверяем date экран с zodiac.enabled и storageKey === condition.screenId
if (selectedScreen.template === "date") {
const dateScreen = selectedScreen as BuilderScreen & { template: "date"; dateInput?: { zodiac?: { enabled?: boolean; storageKey?: string } } };
const zodiacSettings = dateScreen.dateInput?.zodiac;
const storageKey = zodiacSettings?.storageKey?.trim();
// Показываем zodiac селектор если:
// 1. zodiac включен
// 2. storageKey совпадает с condition.screenId (т.е. пользователь выбрал этот storageKey)
return zodiacSettings?.enabled === true && storageKey === condition.screenId;
}
return false;
}, [selectedScreen, condition.screenId]);
const showEmailSelector = selectedScreen?.id === "email";
const showAgeSelector =
selectedScreen?.id === "age" || selectedScreen?.id === "crush-age" || selectedScreen?.id === "current-partner-age";
const conditionType = condition.conditionType ?? "options";
const isUnleashCondition = conditionType === "unleash";
// Обработчики для селекторов
const handleToggleOption = (optionId: string) => {
const currentIds = condition.optionIds || [];
const nextIds = currentIds.includes(optionId)
? currentIds.filter((id) => id !== optionId)
: [...currentIds, optionId];
onChange({ ...condition, optionIds: nextIds });
};
const handleAddCustomOption = (optionId: string) => {
const currentIds = condition.optionIds || [];
if (!currentIds.includes(optionId)) {
onChange({ ...condition, optionIds: [...currentIds, optionId] });
}
};
const handleConditionTypeChange = (newType: "options" | "values" | "unleash") => {
onChange({
...condition,
conditionType: newType,
// Очищаем поля при смене типа
optionIds: newType === "unleash" ? undefined : condition.optionIds,
values: newType === "unleash" ? undefined : condition.values,
unleashFlag: newType === "unleash" ? condition.unleashFlag : undefined,
unleashVariants: newType === "unleash" ? condition.unleashVariants : undefined,
});
};
return (
<div className="space-y-3">
{/* Тип условия */}
<ConditionTypeSelector
condition={condition}
onChange={handleConditionTypeChange}
/>
{/* Unleash AB Test */}
{isUnleashCondition ? (
<UnleashConditionEditor condition={condition} onChange={onChange} />
) : (
<>
{/* Выбор экрана */}
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Экран
</label>
<select
value={condition.screenId}
onChange={(e) => onChange({ ...condition, screenId: e.target.value, optionIds: [] })}
className="w-full h-9 rounded-md border border-border bg-background px-3 text-sm"
>
{allScreens.flatMap((screen) => {
const options = [];
// Для date экранов с zodiac добавляем отдельную опцию для storageKey
if (screen.template === "date") {
const dateScreen = screen as BuilderScreen & { template: "date"; dateInput?: { zodiac?: { enabled?: boolean; storageKey?: string } } };
const zodiacSettings = dateScreen.dateInput?.zodiac;
const storageKey = zodiacSettings?.storageKey?.trim();
if (zodiacSettings?.enabled && storageKey) {
options.push(
<option key={`${screen.id}-zodiac`} value={storageKey}>
{screen.id} (zodiac {storageKey})
</option>
);
}
}
// Обычный экран (всегда)
options.push(
<option key={screen.id} value={screen.id}>
{screen.id} ({screen.template})
</option>
);
return options;
})}
</select>
</div>
{/* Оператор (только для list экранов с несколькими опциями) */}
{conditionOptions.length > 1 && (
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Оператор
</label>
<select
value={condition.operator}
onChange={(e) =>
onChange({
...condition,
operator: e.target.value as "includesAny" | "includesAll",
})
}
className="w-full h-9 rounded-md border border-border bg-background px-3 text-sm"
>
<option value="includesAny">Любое из (OR)</option>
<option value="includesAll">Все из (AND)</option>
</select>
</div>
)}
{/* Zodiac Selector */}
{showZodiacSelector && (
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Знаки зодиака
</label>
<ZodiacSelector
selectedValues={condition.optionIds || []}
onToggleValue={handleToggleOption}
onAddCustomValue={handleAddCustomOption}
/>
</div>
)}
{/* Email Domain Selector */}
{showEmailSelector && (
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Email домены
</label>
<EmailDomainSelector
selectedValues={condition.optionIds || []}
onToggleValue={handleToggleOption}
onAddCustomValue={handleAddCustomOption}
/>
</div>
)}
{/* Age Selector */}
{showAgeSelector && (
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Возраст
</label>
<AgeSelector
selectedValues={condition.optionIds || []}
onToggleValue={handleToggleOption}
onAddCustomValue={handleAddCustomOption}
/>
</div>
)}
{/* Опции для обычных list экранов */}
{!showZodiacSelector &&
!showEmailSelector &&
!showAgeSelector &&
conditionOptions.length > 0 && (
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
Значения
</label>
<div className="space-y-1 max-h-48 overflow-y-auto border border-border rounded-md p-2">
{conditionOptions.map((opt) => (
<label
key={opt.id}
className="flex items-center gap-2 p-2 rounded hover:bg-muted/50 cursor-pointer"
>
<input
type="checkbox"
checked={(condition.optionIds || []).includes(opt.id)}
onChange={() => handleToggleOption(opt.id)}
className="rounded border-border"
/>
<span className="text-sm">
{opt.emoji && <span className="mr-1">{opt.emoji}</span>}
{opt.label}
</span>
</label>
))}
</div>
</div>
)}
</>
)}
</div>
);
}