/* Shared wizard logic — hooks and primitive components used by all 5 variations. Exposes everything to window so other Babel scripts can use them. */ const { useState, useEffect, useRef, useMemo, useCallback } = React; // ─── Hooks ─────────────────────────────────────────────────────────────── function useRoute() { const [route, setRoute] = useState(() => window.parseRoute()); const setLang = useCallback((lang) => setRoute(r => ({ ...r, lang })), []); const setRole = useCallback((role) => setRoute(r => ({ ...r, role })), []); return { ...route, setLang, setRole }; } function useT(lang, role) { return window.getT ? window.getT(lang, role) : (window.I18N[lang] || window.I18N.en); } function useClient(id) { return window.CLIENTS[id] || { name: id || 'friend', contact: '' }; } // Persist wizard answers to localStorage keyed by client ID. function useWizard(clientId, totalSteps, instanceKey = 'v1') { const storageKey = `cf-${instanceKey}-${clientId}`; const [state, setState] = useState(() => { try { const raw = localStorage.getItem(storageKey); if (raw) return JSON.parse(raw); } catch (e) {} return { step: 0, answers: {}, started: false, done: false }; }); useEffect(() => { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch (e) {} }, [state, storageKey]); const next = useCallback(() => setState(s => ({ ...s, step: Math.min(s.step + 1, totalSteps) })), [totalSteps]); const back = useCallback(() => setState(s => ({ ...s, step: Math.max(s.step - 1, 0) })), []); const setAnswer = useCallback((k, v) => setState(s => ({ ...s, answers: { ...s.answers, [k]: v } })), []); const start = useCallback(() => setState(s => ({ ...s, started: true })), []); const finish = useCallback(() => setState(s => ({ ...s, done: true })), []); const reset = useCallback(() => setState({ step: 0, answers: {}, started: false, done: false }), []); return { state, setState, next, back, setAnswer, start, finish, reset }; } // ─── Confetti generator ──────────────────────────────────────────────── function Confetti({ count = 60 }) { const pieces = useMemo(() => Array.from({ length: count }, (_, i) => { const colors = [ 'oklch(0.78 0.13 65)', 'oklch(0.78 0.06 150)', 'oklch(0.85 0.12 90)', 'oklch(0.7 0.16 30)', 'oklch(0.94 0.012 80)', ]; return { left: Math.random() * 100, delay: Math.random() * 1.2, duration: 2.4 + Math.random() * 2, color: colors[i % colors.length], rot: Math.random() * 360, shape: i % 3, }; }), [count]); return (
{pieces.map((p, i) => ( ))}
); } // ─── Lang switcher ───────────────────────────────────────────────────── function LangSwitcher({ lang, onChange }) { return (
{['en', 'ru', 'uk'].map(l => ( ))}
); } // ─── NPS scale ───────────────────────────────────────────────────────── function NpsScale({ value, onChange, onAutoAdvance, t }) { const cat = (n) => { if (n == null) return ''; if (n <= 6) return t.q.nps.cat_detractor || (t.locale === 'ru' ? 'Критик' : t.locale === 'uk' ? 'Критик' : 'Detractor'); if (n <= 8) return t.q.nps.cat_passive || (t.locale === 'ru' ? 'Нейтрал' : t.locale === 'uk' ? 'Нейтрал' : 'Passive'); return t.q.nps.cat_promoter || (t.locale === 'ru' ? 'Промоутер' : t.locale === 'uk' ? 'Промоутер' : 'Promoter'); }; const handle = (n) => { onChange(n); if (onAutoAdvance) setTimeout(() => onAutoAdvance(), 480); }; return (
{Array.from({ length: 11 }, (_, n) => ( ))}
0 — {t.q.nps.scale_low} {t.q.nps.scale_high} — 10
{value != null ? `→ ${cat(value)}` : ''}
); } // ─── Mood ────────────────────────────────────────────────────────────── const MOOD_FACES = ['😞', '😕', '😐', '🙂', '😍']; function MoodPicker({ value, onChange, onAutoAdvance, t }) { const handle = (i) => { onChange(i); if (onAutoAdvance) setTimeout(() => onAutoAdvance(), 600); }; return (
{t.q.mood.moods.map((label, i) => ( ))}
); } // ─── Checks ──────────────────────────────────────────────────────────── function CheckGrid({ options, value = [], onChange }) { const toggle = (opt) => { const has = value.includes(opt); onChange(has ? value.filter(v => v !== opt) : [...value, opt]); }; return (
{options.map(opt => ( ))}
); } // ─── Open question (text + fake voice + fake attachments) ───────────── function OpenQuestion({ value = {}, onChange, t }) { const [recording, setRecording] = useState(false); const [recTime, setRecTime] = useState(0); const recTimer = useRef(); const fileRef = useRef(); const text = value.text || ''; const voice = value.voice || null; // { duration: 7 } const files = value.files || []; useEffect(() => { if (recording) { setRecTime(0); recTimer.current = setInterval(() => setRecTime(t => t + 1), 1000); } else { clearInterval(recTimer.current); } return () => clearInterval(recTimer.current); }, [recording]); const toggleRec = () => { if (recording) { setRecording(false); onChange({ ...value, voice: { duration: recTime || 1 } }); } else { setRecording(true); } }; const onFiles = (e) => { const list = Array.from(e.target.files || []).map(f => ({ name: f.name, size: f.size })); onChange({ ...value, files: [...files, ...list] }); e.target.value = ''; }; const fmtTime = (n) => `${Math.floor(n/60)}:${String(n%60).padStart(2, '0')}`; const fmtSize = (b) => b < 1024 ? `${b}B` : b < 1048576 ? `${Math.round(b/1024)}KB` : `${(b/1048576).toFixed(1)}MB`; return (