diff --git a/public/funnels/soulmate.json b/public/funnels/soulmate.json index 5697653..3c9507d 100644 --- a/public/funnels/soulmate.json +++ b/public/funnels/soulmate.json @@ -4,6 +4,7 @@ "title": "Soulmate V1", "description": "Soulmate", "firstScreenId": "onboarding", + "googleAnalyticsId": "G-4N17LL3BB5", "yandexMetrikaId": "104471567" }, "defaultTexts": { @@ -46,7 +47,62 @@ "align": "center", "color": "default" }, - "variants": [], + "variants": [ + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v0" + ] + } + ], + "overrides": {} + }, + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v1" + ] + } + ], + "overrides": { + "soulmatePortraitsDelivered": { + "image": null, + "mediaUrl": "/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png", + "mediaType": "image" + } + } + }, + { + "conditions": [ + { + "screenId": "onboarding", + "operator": "includesAny", + "conditionType": "unleash", + "unleashFlag": "soulmate-onboarding-image", + "unleashVariants": [ + "v2" + ] + } + ], + "overrides": { + "soulmatePortraitsDelivered": { + "image": null, + "mediaUrl": "/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4", + "mediaType": "video" + } + } + } + ], "soulmatePortraitsDelivered": { "image": "/soulmate-portrait-delivered-male.jpg", "text": { @@ -123,40 +179,7 @@ "showPrivacyTermsConsent": false }, "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "gender", - "conditionType": "unleash", - "operator": "includesAny", - "optionIds": [], - "values": [], - "unleashFlag": "soulmate-gender-info", - "unleashVariants": [ - "v0" - ] - } - ], - "nextScreenId": "partner-gender" - }, - { - "conditions": [ - { - "screenId": "gender", - "conditionType": "unleash", - "operator": "includesAny", - "optionIds": [], - "values": [], - "unleashFlag": "soulmate-gender-info", - "unleashVariants": [ - "v1" - ] - } - ], - "nextScreenId": "gender-info" - } - ], + "rules": [], "defaultNextScreenId": "partner-gender", "isEndScreen": false }, @@ -178,81 +201,7 @@ ], "registrationFieldKey": "profile.gender" }, - "variants": [ - { - "conditions": [ - { - "screenId": "onboarding", - "operator": "includesAny", - "conditionType": "unleash", - "unleashFlag": "soulmate-onboarding-image", - "unleashVariants": [ - "v2" - ] - } - ], - "overrides": { - "title": { - "text": "What’s your gender? v2" - } - } - } - ] - }, - { - "id": "gender-info", - "template": "info", - "header": { - "showBackButton": true, - "show": true - }, - "title": { - "text": "Over 120,000 women have already taken this step to get the most accurate results.", - "show": true, - "font": "manrope", - "weight": "bold", - "size": "2xl", - "align": "center", - "color": "default" - }, - "subtitle": { - "text": "…and gained a deep portrait of their partner.", - "show": true, - "font": "manrope", - "weight": "medium", - "size": "lg", - "align": "center", - "color": "default" - }, - "bottomActionButton": { - "show": true, - "cornerRadius": "3xl", - "showPrivacyTermsConsent": false - }, - "navigation": { - "rules": [], - "defaultNextScreenId": "partner-gender", - "isEndScreen": false - }, - "variables": [], - "variants": [ - { - "conditions": [ - { - "screenId": "gender", - "operator": "includesAny", - "optionIds": [ - "male" - ] - } - ], - "overrides": { - "title": { - "text": "Over 80,000 men have already taken this step to get the most accurate results." - } - } - } - ] + "variants": [] }, { "id": "partner-gender", diff --git a/public/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4 b/public/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4 new file mode 100644 index 0000000..16a94f9 Binary files /dev/null and b/public/images/275472b0-30e0-47d7-a1ab-8090bc9fb236.mp4 differ diff --git a/public/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png b/public/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png new file mode 100644 index 0000000..92b4e56 Binary files /dev/null and b/public/images/90b8c77f-c0cd-475d-a4de-bcabb3708c59.png differ diff --git a/scripts/sync-funnels-from-db.mjs b/scripts/sync-funnels-from-db.mjs index dac8564..4bc8a81 100755 --- a/scripts/sync-funnels-from-db.mjs +++ b/scripts/sync-funnels-from-db.mjs @@ -145,6 +145,14 @@ async function downloadImagesFromDatabase(funnels) { imageUrls.add(screen.image.src); } + // Проверяем soulmatePortraitsDelivered (soulmate экраны) + if (screen.soulmatePortraitsDelivered?.image?.startsWith('/api/images/')) { + imageUrls.add(screen.soulmatePortraitsDelivered.image); + } + if (screen.soulmatePortraitsDelivered?.mediaUrl?.startsWith('/api/images/')) { + imageUrls.add(screen.soulmatePortraitsDelivered.mediaUrl); + } + // Проверяем icon и image в вариантах экрана if (screen.variants && Array.isArray(screen.variants)) { for (const variant of screen.variants) { @@ -157,6 +165,13 @@ async function downloadImagesFromDatabase(funnels) { if (variant.overrides?.image?.src?.startsWith('/api/images/')) { imageUrls.add(variant.overrides.image.src); } + // soulmatePortraitsDelivered в вариантах (soulmate экраны) + if (variant.overrides?.soulmatePortraitsDelivered?.image?.startsWith('/api/images/')) { + imageUrls.add(variant.overrides.soulmatePortraitsDelivered.image); + } + if (variant.overrides?.soulmatePortraitsDelivered?.mediaUrl?.startsWith('/api/images/')) { + imageUrls.add(variant.overrides.soulmatePortraitsDelivered.mediaUrl); + } } } } @@ -234,6 +249,20 @@ function updateImageUrlsInFunnels(funnels, imageMapping) { console.log(`🔗 Updated image URL: ${oldUrl} → ${newUrl}`); } + // Обновляем soulmatePortraitsDelivered (soulmate экраны) + if (screen.soulmatePortraitsDelivered?.image && imageMapping[screen.soulmatePortraitsDelivered.image]) { + const oldUrl = screen.soulmatePortraitsDelivered.image; + const newUrl = imageMapping[oldUrl]; + screen.soulmatePortraitsDelivered.image = newUrl; + console.log(`🔗 Updated soulmate image URL: ${oldUrl} → ${newUrl}`); + } + if (screen.soulmatePortraitsDelivered?.mediaUrl && imageMapping[screen.soulmatePortraitsDelivered.mediaUrl]) { + const oldUrl = screen.soulmatePortraitsDelivered.mediaUrl; + const newUrl = imageMapping[oldUrl]; + screen.soulmatePortraitsDelivered.mediaUrl = newUrl; + console.log(`🔗 Updated soulmate mediaUrl: ${oldUrl} → ${newUrl}`); + } + // Обновляем icon и image в вариантах экрана if (screen.variants && Array.isArray(screen.variants)) { for (const variant of screen.variants) { @@ -252,6 +281,19 @@ function updateImageUrlsInFunnels(funnels, imageMapping) { variant.overrides.image.src = newUrl; console.log(`🔗 Updated variant image URL: ${oldUrl} → ${newUrl}`); } + // soulmatePortraitsDelivered в вариантах (soulmate экраны) + if (variant.overrides?.soulmatePortraitsDelivered?.image && imageMapping[variant.overrides.soulmatePortraitsDelivered.image]) { + const oldUrl = variant.overrides.soulmatePortraitsDelivered.image; + const newUrl = imageMapping[oldUrl]; + variant.overrides.soulmatePortraitsDelivered.image = newUrl; + console.log(`🔗 Updated variant soulmate image URL: ${oldUrl} → ${newUrl}`); + } + if (variant.overrides?.soulmatePortraitsDelivered?.mediaUrl && imageMapping[variant.overrides.soulmatePortraitsDelivered.mediaUrl]) { + const oldUrl = variant.overrides.soulmatePortraitsDelivered.mediaUrl; + const newUrl = imageMapping[oldUrl]; + variant.overrides.soulmatePortraitsDelivered.mediaUrl = newUrl; + console.log(`🔗 Updated variant soulmate mediaUrl: ${oldUrl} → ${newUrl}`); + } } } } diff --git a/src/app/api/images/upload/route.ts b/src/app/api/images/upload/route.ts index 8b2128a..bcbe01f 100644 --- a/src/app/api/images/upload/route.ts +++ b/src/app/api/images/upload/route.ts @@ -4,8 +4,20 @@ import { Image } from '@/lib/models/Image'; import { IS_FRONTEND_ONLY_BUILD } from '@/lib/runtime/buildVariant'; import crypto from 'crypto'; -const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB -const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB (увеличено для видео) +const ALLOWED_TYPES = [ + // Изображения + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/svg+xml', + // Видео + 'video/mp4', + 'video/quicktime', // .mov файлы + 'video/webm' +]; export async function POST(request: NextRequest) { try { @@ -34,14 +46,14 @@ export async function POST(request: NextRequest) { // Валидация файла if (file.size > MAX_FILE_SIZE) { return NextResponse.json( - { error: 'File too large. Maximum size is 5MB' }, + { error: 'File too large. Maximum size is 10MB' }, { status: 400 } ); } if (!ALLOWED_TYPES.includes(file.type)) { return NextResponse.json( - { error: 'Invalid file type. Only images are allowed' }, + { error: 'Invalid file type. Allowed: images (JPG, PNG, WebP, GIF) and videos (MP4, MOV, WebM)' }, { status: 400 } ); } diff --git a/src/components/admin/builder/forms/MediaUpload.tsx b/src/components/admin/builder/forms/MediaUpload.tsx new file mode 100644 index 0000000..9deb980 --- /dev/null +++ b/src/components/admin/builder/forms/MediaUpload.tsx @@ -0,0 +1,381 @@ +"use client"; + +import { useState, useRef, useCallback } from 'react'; +import Image from 'next/image'; +import { Button } from '@/components/ui/button'; +import { TextInput } from '@/components/ui/TextInput/TextInput'; +import { env } from '@/lib/env'; +import { BUILD_VARIANTS } from '@/lib/constants'; +import { Upload, X, ImageIcon, Film, Loader2 } from 'lucide-react'; + +interface UploadedMedia { + id: string; + filename: string; + originalName: string; + url: string; + size: number; + mimetype: string; +} + +type MediaType = "image" | "video" | "gif"; + +interface MediaUploadProps { + currentMediaUrl?: string; + currentMediaType?: MediaType; + onMediaSelect: (url: string, type: MediaType) => void; + onMediaRemove: () => void; + funnelId?: string; +} + +export function MediaUpload({ + currentMediaUrl, + currentMediaType = "image", + onMediaSelect, + onMediaRemove, + funnelId +}: MediaUploadProps) { + const [isUploading, setIsUploading] = useState(false); + const [dragActive, setDragActive] = useState(false); + const [error, setError] = useState(null); + const [uploadedMedia, setUploadedMedia] = useState([]); + const [showGallery, setShowGallery] = useState(false); + const [isLoadingGallery, setIsLoadingGallery] = useState(false); + + const fileInputRef = useRef(null); + + const getMediaType = (mimetype: string): MediaType => { + if (mimetype.includes('gif')) return 'gif'; + if (mimetype.includes('video')) return 'video'; + return 'image'; + }; + + const loadMedia = useCallback(async () => { + if (process.env.NEXT_PUBLIC_FUNNEL_BUILD_VARIANT === 'frontend') { + return; + } + + setIsLoadingGallery(true); + try { + const params = new URLSearchParams(); + if (funnelId) params.append('funnelId', funnelId); + params.append('limit', '20'); + + const response = await fetch(`/api/images?${params}`); + if (response.ok) { + const data = await response.json(); + setUploadedMedia(data.images); + } else { + console.error('Failed to load media'); + } + } catch (error) { + console.error('Error loading media:', error); + } finally { + setIsLoadingGallery(false); + } + }, [funnelId]); + + const handleFileUpload = async (file: File) => { + if (process.env.NEXT_PUBLIC_FUNNEL_BUILD_VARIANT === 'frontend') { + setError('Загрузка файлов недоступна в frontend режиме'); + return; + } + + setIsUploading(true); + setError(null); + + try { + const formData = new FormData(); + formData.append('file', file); + if (funnelId) formData.append('funnelId', funnelId); + + const response = await fetch('/api/images/upload', { + method: 'POST', + body: formData, + }); + + if (response.ok) { + const uploadedFile = await response.json(); + const mediaType = getMediaType(uploadedFile.mimetype); + onMediaSelect(uploadedFile.url, mediaType); + setUploadedMedia(prev => [uploadedFile, ...prev]); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Ошибка загрузки файла'); + } + } catch (error) { + setError('Произошла ошибка при загрузке файла'); + console.error('Upload error:', error); + } finally { + setIsUploading(false); + } + }; + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + handleFileUpload(file); + } + }; + + const handleDrag = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + const file = e.dataTransfer.files?.[0]; + if (file && (file.type.startsWith('image/') || file.type.startsWith('video/'))) { + handleFileUpload(file); + } else { + setError('Пожалуйста, выберите изображение или видео файл'); + } + }; + + const openGallery = () => { + setShowGallery(true); + loadMedia(); + }; + + const isFullMode = env.NEXT_PUBLIC_FUNNEL_BUILD_VARIANT !== BUILD_VARIANTS.FRONTEND; + + const getMediaIcon = () => { + if (currentMediaType === 'video' || currentMediaType === 'gif') { + return ; + } + return ; + }; + + const getMediaLabel = () => { + if (currentMediaType === 'video') return 'Загруженное видео'; + if (currentMediaType === 'gif') return 'Загруженный GIF'; + return 'Загруженное изображение'; + }; + + return ( +
+ {/* Текущий медиа файл */} + {currentMediaUrl && ( +
+
+
+
+ {getMediaIcon()} + + {currentMediaUrl.startsWith('/api/images/') ? getMediaLabel() : currentMediaUrl} + +
+ +
+
+ {/* Превью */} +
+ {currentMediaType === 'image' ? ( + Preview + ) : ( +
+
+ )} + + {!currentMediaUrl && ( +
+ {/* URL Input */} +
+ + { + const url = e.target.value; + // Определяем тип по расширению + let type: MediaType = "image"; + if (url.match(/\.(mp4|mov|webm)$/i)) type = "video"; + else if (url.match(/\.gif$/i)) type = "gif"; + onMediaSelect(url, type); + }} + /> +

+ Поддерживаются: изображения (JPG, PNG, WebP), видео (MP4, MOV, WebM), GIF +

+
+ + {/* Upload Section */} + {isFullMode && ( + <> +
или
+ + {/* Drag & Drop Zone */} +
fileInputRef.current?.click()} + > + + + {isUploading ? ( +
+ +

Загрузка...

+
+ ) : ( +
+
+ + +
+

+ Перетащите медиа файл сюда или нажмите для выбора +

+

+ Изображения, видео (MP4, MOV, WebM), GIF. Максимум 10MB +

+
+ )} +
+ + {/* Gallery Button */} + + + )} +
+ )} + + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Gallery Modal */} + {showGallery && ( +
+
+
+

Выберите медиа файл

+ +
+ + {isLoadingGallery ? ( +
+ +
+ ) : uploadedMedia.length > 0 ? ( +
+ {uploadedMedia.map((media, index) => { + const mediaType = getMediaType(media.mimetype); + return ( +
{ + onMediaSelect(media.url, mediaType); + setShowGallery(false); + }} + > + {mediaType === 'image' ? ( + {media.originalName} + ) : ( +
+ ); + })} +
+ ) : ( +
+ Пока нет загруженных файлов +
+ )} +
+
+ )} +
+ ); +} diff --git a/src/components/admin/builder/templates/SoulmatePortraitScreenConfig.tsx b/src/components/admin/builder/templates/SoulmatePortraitScreenConfig.tsx index a5d5659..9101025 100644 --- a/src/components/admin/builder/templates/SoulmatePortraitScreenConfig.tsx +++ b/src/components/admin/builder/templates/SoulmatePortraitScreenConfig.tsx @@ -3,7 +3,7 @@ import React from "react"; import { TextInput } from "@/components/ui/TextInput/TextInput"; import { Button } from "@/components/ui/button"; -import { ImageUpload } from "@/components/admin/builder/forms/ImageUpload"; +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"; @@ -31,11 +31,13 @@ export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortr updates: Partial> ) => { const base = screen.soulmatePortraitsDelivered ?? {}; + const nextDelivered = { ...base, ...updates }; + + // Важно для вариантов: сохраняем undefined значения! + // Система вариантов использует undefined для отслеживания удаленных полей + onUpdate({ - soulmatePortraitsDelivered: { - ...base, - ...updates, - }, + soulmatePortraitsDelivered: nextDelivered as NonNullable, }); }; @@ -101,13 +103,17 @@ export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortr

Блок доставленных портретов

- Изображение - updateDelivered({ image: url })} - onImageRemove={() => updateDelivered({ image: undefined })} + Основное медиа (изображение/видео/GIF) + 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} /> +

+ Поддержка: изображения (JPG, PNG, WebP), видео (MP4, MOV), GIF с автовоспроизведением без звука +

- Изображение - updateAvatar(index, { src: url })} - onImageRemove={() => updateAvatar(index, { src: "" })} + Изображение (только фото, не видео) + updateAvatar(index, { src: url })} + onMediaRemove={() => updateAvatar(index, { src: "" })} funnelId={screen.id} />
diff --git a/src/components/funnel/FlagVariantFetcher.tsx b/src/components/funnel/FlagVariantFetcher.tsx new file mode 100644 index 0000000..eab1615 --- /dev/null +++ b/src/components/funnel/FlagVariantFetcher.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useVariant } from "@unleash/proxy-client-react"; +import { useEffect } from "react"; + +interface FlagVariantFetcherProps { + flag: string; + onVariantLoaded: (flag: string, variant: string | undefined) => void; +} + +/** + * Компонент для получения варианта одного флага + * Каждый экземпляр этого компонента вызывает useVariant на верхнем уровне + * Это позволяет обходить ограничение правил хуков React + */ +export function FlagVariantFetcher({ flag, onVariantLoaded }: FlagVariantFetcherProps) { + const variant = useVariant(flag); + + useEffect(() => { + onVariantLoaded(flag, variant?.name); + }, [flag, variant?.name, onVariantLoaded]); + + return null; // Этот компонент не рендерит UI +} diff --git a/src/components/funnel/FunnelUnleashWrapper.tsx b/src/components/funnel/FunnelUnleashWrapper.tsx index 591ef99..0bdaba3 100644 --- a/src/components/funnel/FunnelUnleashWrapper.tsx +++ b/src/components/funnel/FunnelUnleashWrapper.tsx @@ -1,9 +1,10 @@ "use client"; -import { useMemo, type ReactNode } from "react"; -import { useFlagsStatus, useVariant } from "@unleash/proxy-client-react"; +import { useState, useMemo, useCallback, type ReactNode } from "react"; +import { useFlagsStatus } from "@unleash/proxy-client-react"; import { UnleashContextProvider } from "@/lib/funnel/unleash"; import { FunnelLoadingScreen } from "./FunnelLoadingScreen"; +import { FlagVariantFetcher } from "./FlagVariantFetcher"; import type { NavigationConditionDefinition } from "@/lib/funnel/types"; interface FunnelUnleashWrapperProps { @@ -66,15 +67,24 @@ export function FunnelUnleashWrapper({ return Array.from(flags); }, [funnel.screens]); - // Получаем варианты для всех флагов - const flagVariants = allFlags.map((flag) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const variant = useVariant(flag); - return { - flag, - variant: variant?.name, - }; - }); + // Состояние для хранения вариантов флагов + const [loadedVariants, setLoadedVariants] = useState>({}); + + // Колбэк для получения варианта от FlagVariantFetcher компонента + const handleVariantLoaded = useCallback((flag: string, variant: string | undefined) => { + if (variant && variant !== "disabled") { + setLoadedVariants((prev) => { + // Обновляем только если значение изменилось + if (prev[flag] !== variant) { + if (process.env.NODE_ENV === "development") { + console.log(`[FunnelUnleashWrapper] Flag "${flag}" = "${variant}"`); + } + return { ...prev, [flag]: variant }; + } + return prev; + }); + } + }, []); // Создаем объект активных вариантов const activeVariants = useMemo(() => { @@ -82,15 +92,12 @@ export function FunnelUnleashWrapper({ return {}; } - const variants: Record = {}; - flagVariants.forEach(({ flag, variant }) => { - if (variant && variant !== "disabled") { - variants[flag] = variant; - } - }); + if (process.env.NODE_ENV === "development") { + console.log("[FunnelUnleashWrapper] Active variants:", loadedVariants); + } - return variants; - }, [flagsReady, flagVariants]); + return loadedVariants; + }, [flagsReady, loadedVariants]); // Показываем loader пока флаги загружаются // Это предотвращает flash of unstyled content @@ -100,6 +107,14 @@ export function FunnelUnleashWrapper({ return ( + {/* Рендерим FlagVariantFetcher для каждого флага */} + {allFlags.map((flag) => ( + + ))} {children} ); diff --git a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx index 75b51e2..3eb53b5 100644 --- a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx +++ b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx @@ -62,6 +62,8 @@ export function SoulmatePortraitTemplate({ {screen.soulmatePortraitsDelivered && ( { - image?: string; + image?: string; // Legacy: используется как background если нет mediaUrl + mediaType?: MediaType; + mediaUrl?: string; // Приоритет над image textProps?: TypographyProps<"p">; avatarsProps?: React.ComponentProps; } export default function SoulmatePortraitsDelivered({ image, + mediaType = "image", + mediaUrl, avatarsProps, textProps, ...props }: SoulmatePortraitsDeliveredProps) { + // Определяем финальный URL медиа (приоритет у mediaUrl) + const finalMediaUrl = mediaUrl || image; + const finalMediaType = mediaUrl ? mediaType : "image"; + const shouldRenderVideo = finalMediaType === "video" || finalMediaType === "gif"; + return (
-
+ {/* Background Media */} + {shouldRenderVideo && finalMediaUrl ? ( +