157 lines
6.0 KiB
TypeScript
157 lines
6.0 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
|
||
import { useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context";
|
||
import { renderScreen } from "@/lib/funnel/screenRenderer";
|
||
import { mergeScreenWithOverrides } from "@/lib/admin/builder/variants";
|
||
|
||
export function BuilderPreview() {
|
||
const selectedScreen = useBuilderSelectedScreen();
|
||
const builderState = useBuilderState();
|
||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||
const [previewVariantIndex, setPreviewVariantIndex] = useState<number | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (!selectedScreen) {
|
||
setSelectedIds([]);
|
||
setPreviewVariantIndex(null);
|
||
return;
|
||
}
|
||
|
||
setSelectedIds((prev) => {
|
||
if (prev.length === 0) {
|
||
return prev;
|
||
}
|
||
return [];
|
||
});
|
||
}, [selectedScreen]);
|
||
|
||
const handleSelectionChange = useCallback((ids: string[]) => {
|
||
setSelectedIds((prev) => {
|
||
if (prev.length === ids.length && prev.every((value, index) => value === ids[index])) {
|
||
return prev;
|
||
}
|
||
return ids;
|
||
});
|
||
}, []);
|
||
|
||
|
||
const variants = useMemo(() => selectedScreen?.variants ?? [], [selectedScreen]);
|
||
|
||
useEffect(() => {
|
||
setPreviewVariantIndex(null);
|
||
}, [selectedScreen]);
|
||
|
||
const previewScreen = useMemo(() => {
|
||
if (!selectedScreen) {
|
||
return null;
|
||
}
|
||
|
||
if (previewVariantIndex === null) {
|
||
return selectedScreen;
|
||
}
|
||
|
||
const variant = variants[previewVariantIndex];
|
||
if (!variant) {
|
||
return selectedScreen;
|
||
}
|
||
|
||
return mergeScreenWithOverrides(selectedScreen, variant.overrides ?? {});
|
||
}, [previewVariantIndex, selectedScreen, variants]);
|
||
|
||
const renderScreenPreview = useCallback(() => {
|
||
if (!previewScreen) return null;
|
||
|
||
try {
|
||
// Use the same renderer as FunnelRuntime for 1:1 accuracy
|
||
return renderScreen({
|
||
screen: previewScreen,
|
||
selectedOptionIds: selectedIds,
|
||
onSelectionChange: handleSelectionChange,
|
||
onContinue: () => {}, // Mock continue handler for preview
|
||
canGoBack: true, // Show back button in preview
|
||
onBack: () => {}, // Mock back handler for preview
|
||
screenProgress: { current: 1, total: 10 }, // Mock progress for preview
|
||
defaultTexts: builderState.defaultTexts, // Use real defaultTexts from builder
|
||
});
|
||
} catch (error) {
|
||
console.error('Error rendering preview:', error);
|
||
return (
|
||
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">
|
||
Ошибка при отображении превью: {error instanceof Error ? error.message : 'Неизвестная ошибка'}
|
||
</div>
|
||
);
|
||
}
|
||
}, [previewScreen, selectedIds, handleSelectionChange, builderState.defaultTexts]);
|
||
|
||
const preview = useMemo(() => {
|
||
if (!previewScreen) {
|
||
return (
|
||
<div className="flex items-center justify-center mx-auto" style={{ height: '600px', width: '320px' }}>
|
||
<div className="flex items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 text-sm text-muted-foreground w-full h-full">
|
||
Выберите экран для предпросмотра
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Увеличим высоту чтобы кнопка поместилась полностью
|
||
const PREVIEW_WIDTH = 320;
|
||
const PREVIEW_HEIGHT = 750; // Увеличено с ~694px до 750px для BottomActionButton
|
||
|
||
return (
|
||
<div className="mx-auto space-y-3" style={{ width: PREVIEW_WIDTH }}>
|
||
{variants.length > 0 && (
|
||
<div className="rounded-lg border border-border/60 bg-background/90 p-2 text-xs text-muted-foreground">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="font-semibold uppercase tracking-wide text-muted-foreground/80">
|
||
Превью варианта
|
||
</span>
|
||
<select
|
||
className="rounded-md border border-border bg-background px-2 py-1 text-xs"
|
||
value={previewVariantIndex === null ? "base" : String(previewVariantIndex)}
|
||
onChange={(event) =>
|
||
setPreviewVariantIndex(event.target.value === "base" ? null : Number(event.target.value))
|
||
}
|
||
>
|
||
<option value="base">Основной экран</option>
|
||
{variants.map((variant, index) => (
|
||
<option key={index} value={index}>
|
||
Вариант {index + 1}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
{previewVariantIndex !== null && (
|
||
<div className="mt-2 rounded border border-blue-200 bg-blue-50 px-2 py-1 text-[11px] text-blue-700 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
|
||
⚠️ Превью принудительно показывает вариант. В реальной воронке он показывается только при выполнении условий.
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Mobile Frame - Simple Border */}
|
||
<div
|
||
className="relative bg-white rounded-2xl border-4 border-gray-300 shadow-lg mx-auto"
|
||
style={{
|
||
height: PREVIEW_HEIGHT,
|
||
width: PREVIEW_WIDTH,
|
||
overflow: 'hidden', // Hide anything that goes outside
|
||
contain: 'layout style paint', // CSS containment
|
||
isolation: 'isolate', // Create new stacking context
|
||
transform: 'translateZ(0)' // Force new layer
|
||
}}
|
||
>
|
||
{/* Screen Content with scroll */}
|
||
<div className="w-full h-full overflow-y-auto overflow-x-hidden [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||
{renderScreenPreview()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}, [previewScreen, renderScreenPreview, variants, previewVariantIndex]);
|
||
|
||
return preview;
|
||
}
|