w-funnel/src/components/admin/builder/templates/SoulmatePortraitScreenConfig.tsx
2025-10-21 22:07:55 +02:00

237 lines
10 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 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">
Экран &ldquo;Soulmate Portrait&rdquo; предназначен для отображения результатов анализа совместимости
или характеристик идеального партнера на основе ответов пользователя в воронке.
</p>
</div>
</div>
);
}