import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { buildStyles, CircularProgressbar } from 'react-circular-progressbar'; import { FaChevronRight } from 'react-icons/fa'; import { Link } from 'react-router-dom'; import { useAppStateContext } from '../context/user.data.context'; import { IPopupData, IPopupTranslation, IQuickAction, } from '../service/popup/popup.interface'; import { DEFAULT_LANG_VALUE } from '../utils/popupTranslations'; import { getMedia, getPopupData, getPopupImage, saveLogo, savePopupData, } from '../service/popup/popup.service'; import { hexToRgba } from '../utils/colors'; import { DEFAULT_QUICK_ACTIONS } from '../utils/quickAction'; import CustomizationSettings from '../components/settings/CustomizationSettings'; import CustomizationStyles from '../components/settings/CustomizationStyles'; import { PreviewSection } from '../components/preview/PreviewSection'; import PromptConfigSettings from '../components/settings/PromptConfigSettings'; import { PlanType } from '../types'; const TabIcons: Record = { style: ( ), setting: ( ), 'prompt-config': ( ), }; const Customization = (): JSX.Element => { const [selected, setSelected] = useState(0); const [position, setPosition] = useState('left'); const [headerMessage, setHeaderMessage] = useState(null); const [expandedHeaderMessage, setExpandedHeaderMessage] = useState< string | null >(null); const [useStock, setUseStock] = useState(true); const [textColor, setTextColor] = useState('#000000'); const [buttonColor, setButtonColor] = useState('#000000'); const [popupBorderColorLeft, setPopupBorderColorLeft] = useState('#7897ff'); const [popupBorderColorRight, setPopupBorderColorRight] = useState('#eb6362'); const [image, setImage] = useState(null); const [imageName, setImageName] = useState(''); const { user, token } = useAppStateContext(); const promptConfigPlans = useMemo( () => [PlanType.SCALING, PlanType.HYPER_SCALING, PlanType.ENTERPRISE], [] ); const [cartIconColor, setCartIconColor] = useState('#FFFFFF'); const [restoreOnNavigation, setRestoreOnNavigation] = useState(true); const [defaultState, setDefaultState] = useState('minimized'); const [defaultStateMobile, setDefaultStateMobile] = useState('minimized'); const [size, setSize] = useState<'small' | 'medium' | 'big'>('medium'); const [showPrices, setShowPrices] = useState(true); const [showAddToCartButton, setShowAddToCartButton] = useState(true); const [sourceVideo, setSourceVideo] = useState(null); const [popup, setPopup] = useState(null); const [quickActions, setQuickActions] = useState( DEFAULT_QUICK_ACTIONS ); const [placeholders, setPlaceholders] = useState(['']); const [isUploadingIcon, setIsUploadingIcon] = useState(false); // Per-language copy overrides. Root state (above) is the default copy; // when the merchant picks a language, edits are routed to translations[lang]. const [selectedLang, setSelectedLangState] = useState(DEFAULT_LANG_VALUE); const [translations, setTranslations] = useState< Record >({}); const isDefaultLang = selectedLang === DEFAULT_LANG_VALUE; const setSelectedLang = useCallback( (lang: string) => { if (lang !== DEFAULT_LANG_VALUE && !translations[lang]) { setTranslations(prev => ({ ...prev, [lang]: { header_message: headerMessage ?? '', expanded_header_message: expandedHeaderMessage ?? '', quick_actions: quickActions.map(qa => ({ key: qa.key, label: qa.label, query: qa.query, })), placeholders: placeholders.slice(), }, })); } setSelectedLangState(lang); }, [ translations, headerMessage, expandedHeaderMessage, quickActions, placeholders, ] ); const activeTranslation: IPopupTranslation | undefined = isDefaultLang ? undefined : translations[selectedLang]; const displayHeaderMessage = isDefaultLang ? (headerMessage ?? '') : (activeTranslation?.header_message ?? ''); const displayExpandedHeaderMessage = isDefaultLang ? (expandedHeaderMessage ?? '') : (activeTranslation?.expanded_header_message ?? ''); const displayPlaceholders = isDefaultLang ? placeholders : (activeTranslation?.placeholders ?? []); const displayQuickActions: IQuickAction[] = isDefaultLang ? quickActions : quickActions.map(qa => { const tr = activeTranslation?.quick_actions?.find( entry => entry.key === qa.key ); return tr ? { ...qa, label: tr.label, query: tr.query } : qa; }); const updateTranslationField = useCallback( (lang: string, patch: Partial) => { setTranslations(prev => ({ ...prev, [lang]: { ...prev[lang], ...patch }, })); }, [] ); const handleSetHeaderMessage = useCallback( (value: string) => { const next = value.slice(0, 90); if (isDefaultLang) setHeaderMessage(next); else updateTranslationField(selectedLang, { header_message: next }); }, [isDefaultLang, selectedLang, updateTranslationField] ); const handleSetExpandedHeaderMessage = useCallback( (value: string) => { const next = value.slice(0, 130); if (isDefaultLang) setExpandedHeaderMessage(next); else updateTranslationField(selectedLang, { expanded_header_message: next, }); }, [isDefaultLang, selectedLang, updateTranslationField] ); const handleSetPlaceholders = useCallback( (next: string[]) => { if (isDefaultLang) setPlaceholders(next); else updateTranslationField(selectedLang, { placeholders: next }); }, [isDefaultLang, selectedLang, updateTranslationField] ); const handleRemoveTranslation = useCallback(() => { if (isDefaultLang) return; const langToRemove = selectedLang; setTranslations(prev => { if (!(langToRemove in prev)) return prev; const copy = { ...prev }; delete copy[langToRemove]; return copy; }); setSelectedLangState(DEFAULT_LANG_VALUE); }, [isDefaultLang, selectedLang]); const [previewTab, setPreviewTab] = useState<{ index: number | undefined; tick: number; }>({ index: undefined, tick: 0 }); const [blobExpanded, setBlobExpanded] = useState( undefined ); const [blobSkipIntro, setBlobSkipIntro] = useState( undefined ); const isInitialMount = useRef(true); const goToPreviewTab = useCallback((tabIndex: number) => { setPreviewTab(prev => ({ index: tabIndex, tick: prev.tick + 1 })); }, []); const handleWidgetStateChange = useCallback( (state: string) => { if (state === 'expanded') { goToPreviewTab(0); setBlobExpanded(undefined); setBlobSkipIntro(undefined); } else if (state === 'partial') { goToPreviewTab(1); setBlobExpanded(true); setBlobSkipIntro(false); } else { goToPreviewTab(1); setBlobExpanded(false); setBlobSkipIntro(true); } }, [goToPreviewTab] ); useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; return; } goToPreviewTab(1); }, [size, goToPreviewTab]); const handleWidgetPosition = useCallback( (pos: string) => { if (pos === 'left' || pos === 'right') { goToPreviewTab(1); } }, [goToPreviewTab] ); const wrappedPopupPosition = useCallback( (pos: string) => { setPosition(pos); handleWidgetPosition(pos); }, [handleWidgetPosition] ); const updateQuickAction = useCallback( (idx: number, patch: Partial) => { setQuickActions(prev => prev.map((a, i) => (i === idx ? { ...a, ...patch } : a)) ); }, [] ); const handleUpdateQuickAction = useCallback( (idx: number, patch: Partial) => { const hasRootKeys = patch.icon_url !== undefined || patch.visible !== undefined; const hasTextKeys = patch.label !== undefined || patch.query !== undefined; if (hasRootKeys || isDefaultLang) updateQuickAction(idx, patch); if (isDefaultLang || !hasTextKeys) return; const root = quickActions[idx]; if (!root?.key) return; setTranslations(prev => { const list = (prev[selectedLang]?.quick_actions ?? []).slice(); const i = list.findIndex(qa => qa.key === root.key); const base = i >= 0 ? list[i] : { key: root.key, label: root.label, query: root.query || root.label, }; const merged = { ...base, ...(patch.label !== undefined ? { label: patch.label } : {}), ...(patch.query !== undefined ? { query: patch.query } : {}), }; if (i >= 0) list[i] = merged; else list.push(merged); return { ...prev, [selectedLang]: { ...prev[selectedLang], quick_actions: list }, }; }); }, [isDefaultLang, selectedLang, quickActions, updateQuickAction] ); const uploadQuickActionIcon = useCallback( async (idx: number, file: File) => { if (!user) return; setIsUploadingIcon(true); try { const mediaType = `QUICK_ACTION_${idx}`; const formData = new FormData(); formData.append('file', file); formData.append('file_type', 'LOGO'); formData.append('model_type', mediaType); formData.append('model_id', user.id); await saveLogo(formData); const mediaRes = await getMedia(user.id, mediaType); const iconUrl = mediaRes?.media?.file_path ?? null; if (iconUrl) { updateQuickAction(idx, { icon_url: iconUrl }); } } catch (error) { console.error('Failed to upload quick action icon:', error); } finally { setIsUploadingIcon(false); } }, [user, updateQuickAction] ); const saveQuickActions = useCallback( async (actions: IQuickAction[]) => { if (!popup) return; const originalActions = popup.settings?.quick_actions?.length ? popup.settings.quick_actions : DEFAULT_QUICK_ACTIONS; const hasChanges = actions.filter((currentAction, index) => { const originalAction = originalActions[index]; if (!originalAction) return true; return ( currentAction.label !== originalAction.label || currentAction.query !== originalAction.query || currentAction.visible !== originalAction.visible || currentAction.icon_url !== originalAction.icon_url || currentAction.key !== originalAction.key ); }); if (!hasChanges) { return; } try { await savePopupData({ color: popup.color || hexToRgba(textColor), secondary_color: popup.secondary_color || hexToRgba(buttonColor), settings: { ...popup.settings, quick_actions: actions, translations, }, position, header_message: popup.header_message || 'Keep browsing to unlock', unlocked_popup_message: popup.unlocked_popup_message || 'Premium Recommendations Unlocked', expanded_header_message: popup.expanded_header_message, buy_now_button_message: popup.buy_now_button_message || 'Add to cart', locked_popup_message: popup.locked_popup_message || 'Keep browsing to unlock', toggle_button_message: popup.toggle_button_message || 'Discover', search_more_message: popup.search_more_message || "Keep exploring! We'll have more personalized picks for you soon.", like_button_message: popup.like_button_message || 'like', dislike_button_message: popup.dislike_button_message || 'dislike', loading_message: popup.loading_message || 'Reveal Secret Deals!', come_back_button_message: popup.come_back_button_message || 'Come back for more', in_stock_message: popup.in_stock_message || 'In stock', link_copied_message: popup.link_copied_message || 'Link copied', engagement_messages: popup.engagement_messages, source_video: popup.source_video, use_stock: popup.use_stock ?? true, default_state: popup.default_state || 'minimized', default_state_mobile: popup.default_state_mobile || 'minimized', }); } catch (error) { console.error('Failed to save quick actions:', error); } }, [popup, textColor, buttonColor, position, translations] ); const wrappedSetDefaultState = useCallback( (state: string) => { setDefaultState(state); handleWidgetStateChange(state); }, [handleWidgetStateChange] ); const wrappedSetDefaultStateMobile = useCallback( (state: string) => { setDefaultStateMobile(state); handleWidgetStateChange(state); }, [handleWidgetStateChange] ); const hasPromptConfigAccess = useMemo(() => { const normalizedPlan = (user?.plan_snapshot || user?.plan || '') .toString() .toUpperCase(); return promptConfigPlans.includes(normalizedPlan as PlanType); }, [promptConfigPlans, user?.plan_snapshot, user?.plan]); const tabs = useMemo(() => { const baseTabs = [ { id: 'style', content: 'Popup Styles', panelID: 'style' }, { id: 'setting', content: 'Settings', panelID: 'setting' }, ]; if (hasPromptConfigAccess) { baseTabs.push({ id: 'prompt-config', content: 'Prompt Config', panelID: 'prompt-config', }); } return baseTabs; }, [hasPromptConfigAccess]); useEffect(() => { if (!popup && token) fetchPopData(); }, [token, popup]); useEffect((): void => { if (token && !image && user) fetchPopupImage(user?.id); }, [image, token, user]); useEffect(() => { const styleId = 'dynamic-gradient-style'; const oldStyle = document.getElementById(styleId); if (oldStyle) oldStyle.remove(); const style = document.createElement('style'); style.id = styleId; style.innerHTML = ` @keyframes dynamicGradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .dynamic-gradient-bg { background: linear-gradient(-45deg, ${popupBorderColorLeft}, ${popupBorderColorRight}, ${popupBorderColorLeft}, ${popupBorderColorRight}); background-size: 400% 400%; animation: dynamicGradient 15s ease infinite; } `; document.head.appendChild(style); return () => { const el = document.getElementById(styleId); if (el) el.remove(); }; }, [popupBorderColorLeft, popupBorderColorRight]); const fetchPopupImage = async (userId: string): Promise => { try { const response = await getPopupImage({ modelId: userId }); if (response?.media) { setImage(response?.media?.file_path as string); setImageName(response?.media?.file_name as string); } } catch (error) { console.log(error); } }; const fetchPopData = async (): Promise => { try { const response = await getPopupData(); if (response?.success && response?.popup) { setPopup(response.popup); setHeaderMessage(response?.popup?.engagement_messages?.header_message); setExpandedHeaderMessage( response?.popup?.engagement_messages?.expanded_header_message ); setPosition(response?.popup?.position || 'left'); setSourceVideo(response?.popup?.source_video); setUseStock(response?.popup?.use_stock || true); setDefaultState(response?.popup?.default_state || 'minimized'); setDefaultStateMobile( response?.popup?.default_state_mobile || 'minimized' ); if (response?.popup?.settings) { setCartIconColor( response?.popup?.settings?.cart_icon_color || '#FFFFFF' ); setTextColor(response?.popup?.settings?.text_color); setButtonColor(response?.popup?.settings?.button_color); setPopupBorderColorLeft( response?.popup?.settings?.gradient_colors[0] || '#ff7e5f' ); setPopupBorderColorRight( response?.popup?.settings?.gradient_colors[1] || '#feb47b' ); setRestoreOnNavigation( response?.popup?.settings?.restore_on_navigation || true ); setSize(response?.popup?.settings?.size || 'medium'); setShowPrices(response?.popup?.settings?.show_prices || true); setShowAddToCartButton( response?.popup?.settings?.show_add_to_cart_button || true ); const savedQA = response?.popup?.settings?.quick_actions; if (savedQA?.length) { setQuickActions( DEFAULT_QUICK_ACTIONS.map((def, i) => savedQA[i] ? { ...def, ...savedQA[i] } : def ) ); } const savedPlaceholders = response?.popup?.settings?.placeholders; if (Array.isArray(savedPlaceholders) && savedPlaceholders.length) { setPlaceholders(savedPlaceholders.slice(0, 5)); } const savedTranslations = response?.popup?.settings?.translations; if ( savedTranslations && typeof savedTranslations === 'object' && !Array.isArray(savedTranslations) ) { setTranslations( savedTranslations as Record ); } } if (user?.id) { try { const iconResults = await Promise.all( [0, 1, 2, 3].map(i => getMedia(user.id, `QUICK_ACTION_${i}`).catch(() => null) ) ); setQuickActions(prev => prev.map((a, i) => ({ ...a, icon_url: iconResults[i]?.media?.file_path ?? a.icon_url, })) ); } catch { // icon fetch is non-critical } } } } catch (error) { console.log(error); } }; useEffect(() => { if (selected > tabs.length - 1) { setSelected(0); } }, [selected, tabs.length]); const onResetDefaultColors = () => { setTextColor('#000000'); setButtonColor('#000000'); setCartIconColor('#FFFFFF'); setPopupBorderColorLeft('#7897ff'); setPopupBorderColorRight('#eb6362'); }; return (
{user?.step && user?.step <= 2 ? (
Getting started {user?.step >= 2 ? ( 2 of 2 steps completed ) : ( Basic steps to raise your conversion rate )}

Complete these basic important steps

Basic yet important steps to start using Recomaze and raise your conversion rate

{user?.step && user?.step <= 1 ? 'Continue setup' : 'Review setup'}

Customize Recomaze on Your Storefront

) : null}
{tabs.map((tab, key) => { const isActive = selected === key; return ( ); })}
{tabs[selected]?.id === 'style' ? ( { setButtonColor(color); goToPreviewTab(3); }} setPopupBorderColorLeft={color => { setPopupBorderColorLeft(color); goToPreviewTab(3); }} setPopupBorderColorRight={color => { setPopupBorderColorRight(color); goToPreviewTab(3); }} setTextColor={color => { setTextColor(color); goToPreviewTab(3); }} setCartIconColor={color => { setCartIconColor(color); goToPreviewTab(3); }} onResetDefaultColors={onResetDefaultColors} headerMessage={displayHeaderMessage} sourceVideo={sourceVideo} expandedHeaderMessage={displayExpandedHeaderMessage} cartIconColor={cartIconColor} restoreOnNavigation={restoreOnNavigation} defaultState={defaultState as any} size={size} showAddToCartButton={showAddToCartButton} showPrices={showPrices} quickActions={displayQuickActions} /> ) : null} {tabs[selected]?.id === 'setting' ? ( { handleSetPlaceholders(next); goToPreviewTab(0); }} /> ) : null} {tabs[selected]?.id === 'prompt-config' ? ( ) : null}
); }; export default Customization;