237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import React from "react";
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { Button } from "@/components/ui/button";
|
||
import { MediaUpload } from "@/components/admin/builder/forms/MediaUpload";
|
||
import { Plus, Trash2 } from "lucide-react";
|
||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||
|
||
interface SoulmatePortraitScreenConfigProps {
|
||
screen: BuilderScreen & { template: "soulmate" };
|
||
onUpdate: (updates: Partial<SoulmatePortraitScreenDefinition>) => void;
|
||
}
|
||
|
||
export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortraitScreenConfigProps) {
|
||
type Delivered = NonNullable<SoulmatePortraitScreenDefinition["soulmatePortraitsDelivered"]>;
|
||
type Avatar = NonNullable<Delivered["avatars"]>[number];
|
||
type DeliveredText = NonNullable<Delivered["text"]>;
|
||
|
||
const updateDescription = (updates: Partial<SoulmatePortraitScreenDefinition["description"]>) => {
|
||
onUpdate({
|
||
description: screen.description ? {
|
||
...screen.description,
|
||
...updates,
|
||
} : { text: "", ...updates },
|
||
});
|
||
};
|
||
|
||
const updateDelivered = (
|
||
updates: Partial<NonNullable<SoulmatePortraitScreenDefinition["soulmatePortraitsDelivered"]>>
|
||
) => {
|
||
const base = screen.soulmatePortraitsDelivered ?? {};
|
||
const nextDelivered = { ...base, ...updates };
|
||
|
||
// Важно для вариантов: сохраняем undefined значения!
|
||
// Система вариантов использует undefined для отслеживания удаленных полей
|
||
|
||
onUpdate({
|
||
soulmatePortraitsDelivered: nextDelivered as NonNullable<SoulmatePortraitScreenDefinition["soulmatePortraitsDelivered"]>,
|
||
});
|
||
};
|
||
|
||
const updateDeliveredText = (
|
||
updates: Partial<DeliveredText>
|
||
) => {
|
||
const currentText = (screen.soulmatePortraitsDelivered?.text ?? { text: "" }) as DeliveredText;
|
||
const nextText = { ...currentText, ...(updates as object) } as DeliveredText;
|
||
updateDelivered({ text: nextText });
|
||
};
|
||
|
||
const addAvatar = () => {
|
||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||
updateDelivered({ avatars: [...avatars, { src: "", alt: "", fallbackText: "" }] });
|
||
};
|
||
|
||
const removeAvatar = (index: number) => {
|
||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||
updateDelivered({ avatars: avatars.filter((_, i) => i !== index) });
|
||
};
|
||
|
||
const updateAvatar = (
|
||
index: number,
|
||
updates: Partial<Avatar>
|
||
) => {
|
||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||
const next = avatars.map((a, i) => (i === index ? { ...a, ...updates } : a));
|
||
updateDelivered({ avatars: next });
|
||
};
|
||
|
||
const addTextListItem = () => {
|
||
const items = screen.textList?.items ?? [];
|
||
onUpdate({ textList: { items: [...items, { text: "" }] } });
|
||
};
|
||
|
||
const removeTextListItem = (index: number) => {
|
||
const items = screen.textList?.items ?? [];
|
||
onUpdate({ textList: { items: items.filter((_, i) => i !== index) } });
|
||
};
|
||
|
||
const updateTextListItem = (
|
||
index: number,
|
||
updates: Partial<NonNullable<SoulmatePortraitScreenDefinition["textList"]>["items"][number]>
|
||
) => {
|
||
const items = screen.textList?.items ?? [];
|
||
const next = items.map((it, i) => (i === index ? { ...it, ...updates } : it));
|
||
onUpdate({ textList: { items: next } });
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<h4 className="text-sm font-medium text-slate-700 mb-3">Описание портрета</h4>
|
||
<TextInput
|
||
label="Текст описания"
|
||
placeholder="Ваш идеальный партнер найден на основе анализа ваших ответов"
|
||
value={screen.description?.text || ""}
|
||
onChange={(e) => updateDescription({ text: e.target.value })}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="text-sm font-medium text-slate-700 mb-3">Блок доставленных портретов</h4>
|
||
<div className="space-y-3">
|
||
<div>
|
||
<span className="text-xs font-medium text-muted-foreground mb-2 block">Основное медиа (изображение/видео/GIF)</span>
|
||
<MediaUpload
|
||
currentMediaUrl={screen.soulmatePortraitsDelivered?.mediaUrl || screen.soulmatePortraitsDelivered?.image}
|
||
currentMediaType={screen.soulmatePortraitsDelivered?.mediaType}
|
||
onMediaSelect={(url, type) => updateDelivered({ mediaUrl: url, mediaType: type, image: null as unknown as undefined })}
|
||
onMediaRemove={() => updateDelivered({ mediaUrl: null as unknown as undefined, mediaType: null as unknown as undefined, image: null as unknown as undefined })}
|
||
funnelId={screen.id}
|
||
/>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
Поддержка: изображения (JPG, PNG, WebP), видео (MP4, MOV), GIF с автовоспроизведением без звука
|
||
</p>
|
||
</div>
|
||
<TextInput
|
||
label="Текст под изображением"
|
||
placeholder="soulmate portraits delivered today"
|
||
value={screen.soulmatePortraitsDelivered?.text?.text || ""}
|
||
onChange={(e) => updateDeliveredText({ text: e.target.value })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h4 className="text-sm font-medium text-slate-700">Аватары</h4>
|
||
<Button type="button" variant="outline" onClick={addAvatar} className="flex items-center gap-2 text-sm px-3 py-1">
|
||
<Plus className="w-4 h-4" /> Добавить
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{(screen.soulmatePortraitsDelivered?.avatars ?? []).map((avatar, index) => (
|
||
<div key={index} className="border rounded-lg p-4 space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h5 className="text-sm font-medium text-slate-600">Аватар {index + 1}</h5>
|
||
<Button type="button" variant="ghost" onClick={() => removeAvatar(index)} className="text-red-600 hover:text-red-700 text-sm px-2 py-1">
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div>
|
||
<span className="text-xs font-medium text-muted-foreground mb-2 block">Изображение (только фото, не видео)</span>
|
||
<MediaUpload
|
||
currentMediaUrl={avatar.src}
|
||
currentMediaType="image"
|
||
onMediaSelect={(url) => updateAvatar(index, { src: url })}
|
||
onMediaRemove={() => updateAvatar(index, { src: "" })}
|
||
funnelId={screen.id}
|
||
/>
|
||
</div>
|
||
<TextInput
|
||
label="Alt"
|
||
placeholder="Описание"
|
||
value={avatar.alt || ""}
|
||
onChange={(e) => updateAvatar(index, { alt: e.target.value })}
|
||
/>
|
||
<TextInput
|
||
label="Fallback текст"
|
||
placeholder="Напр. 900+"
|
||
value={avatar.fallbackText || ""}
|
||
onChange={(e) => updateAvatar(index, { fallbackText: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">Можно указать изображение или fallback текст (или оба).</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{(screen.soulmatePortraitsDelivered?.avatars ?? []).length === 0 && (
|
||
<div className="text-center py-6 text-slate-500">
|
||
<p>Нет аватаров</p>
|
||
<Button type="button" variant="outline" onClick={addAvatar} className="mt-2 text-sm px-3 py-1">
|
||
Добавить первый
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h4 className="text-sm font-medium text-slate-700">Список текстов</h4>
|
||
<Button type="button" variant="outline" onClick={addTextListItem} className="flex items-center gap-2 text-sm px-3 py-1">
|
||
<Plus className="w-4 h-4" /> Добавить
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{(screen.textList?.items ?? []).map((item, index) => (
|
||
<div key={index} className="space-y-1">
|
||
<TextInput
|
||
label={`Элемент ${index + 1}`}
|
||
placeholder="Текст элемента"
|
||
value={item.text || ""}
|
||
onChange={(e) => updateTextListItem(index, { text: e.target.value })}
|
||
/>
|
||
<div className="flex justify-end">
|
||
<Button type="button" variant="ghost" onClick={() => removeTextListItem(index)} className="text-red-600 hover:text-red-700 text-sm px-2 py-1">
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{(screen.textList?.items ?? []).length === 0 && (
|
||
<div className="text-center py-6 text-slate-500">
|
||
<p>Пока нет элементов</p>
|
||
<Button type="button" variant="outline" onClick={addTextListItem} className="mt-2 text-sm px-3 py-1">
|
||
Добавить первый
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="text-sm font-medium text-slate-700 mb-3">Информация</h4>
|
||
<div className="text-xs text-muted-foreground">
|
||
<p>• PrivacyTermsConsent настраивается через bottomActionButton.showPrivacyTermsConsent</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 bg-blue-50 rounded-lg">
|
||
<h4 className="text-sm font-medium text-blue-900 mb-2">💡 Назначение экрана</h4>
|
||
<p className="text-sm text-blue-700">
|
||
Экран “Soulmate Portrait” предназначен для отображения результатов анализа совместимости
|
||
или характеристик идеального партнера на основе ответов пользователя в воронке.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|