// Kicks.jsx (function () { const { useState, useEffect, useRef } = React; const { t, formatDur, formatTime } = window.AliaData; const { getKickSessions, saveKickSession, isLoggedIn } = window.AliaAPI; // Обратная связь на тап: слышимый клик (Web Audio) + вибрация (где доступна). let _audioCtx = null; function getCtx() { try { if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (_audioCtx.state === 'suspended') _audioCtx.resume(); return _audioCtx; } catch (_) { return null; } } // Короткий приятный «бип» — слышимая обратная связь function playClick() { const ctx = getCtx(); if (!ctx) return; try { const t0 = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(660, t0); osc.frequency.exponentialRampToValueAtTime(880, t0 + 0.06); gain.gain.setValueAtTime(0.0001, t0); gain.gain.exponentialRampToValueAtTime(0.22, t0 + 0.012); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.16); osc.connect(gain); gain.connect(ctx.destination); osc.start(t0); osc.stop(t0 + 0.18); } catch (_) {} } function vibrate() { try { if (navigator.vibrate) navigator.vibrate(35); } catch (_) {} } function haptic() { playClick(); vibrate(); } // «Разогрев» аудио на первом жесте (iOS требует пользовательского события) function warmAudio() { getCtx(); } const STORAGE_KEY = 'alia_kick_sessions'; const GOAL = 10; const LIMIT_SECS = 2 * 60 * 60; // 2 hours function loadSessions() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } } function saveSessions(arr) { localStorage.setItem(STORAGE_KEY, JSON.stringify(arr.slice(0, 20))); } // Map API session format → local format function apiToLocal(s) { return { id: s.id, mode: s.mode, startTime: s.created_at, kicks: Array.from({ length: s.kicks }, (_, i) => ({ time: s.created_at })), endTime: s.created_at, ended: true, durationSec: s.duration_sec, }; } function KicksScreen({ lang }) { const [mode, setMode] = useState('free'); // free | goal const [sessions, setSessions] = useState(loadSessions); const [active, setActive] = useState(null); // current session or null const [elapsed, setElapsed] = useState(0); // seconds since session start const [flash, setFlash] = useState(false); // visual tap feedback const timerRef = useRef(null); const flashRef = useRef(null); // Load history from API on mount useEffect(() => { if (!isLoggedIn() || localStorage.getItem('alia_guest')) return; getKickSessions() .then(data => { if (data.sessions && data.sessions.length > 0) { const mapped = data.sessions.map(apiToLocal); setSessions(mapped); saveSessions(mapped); } }) .catch(() => {/* use cached */}); }, []); // eslint-disable-line // tick useEffect(() => { if (active) { timerRef.current = setInterval(() => { setElapsed(Math.floor((Date.now() - new Date(active.startTime)) / 1000)); }, 500); } return () => clearInterval(timerRef.current); }, [active]); function startSession() { warmAudio(); // активируем звук на пользовательском жесте const sess = { id: Date.now(), mode, startTime: new Date().toISOString(), kicks: [], ended: false }; setActive(sess); setElapsed(0); } function addKick() { if (!active) return; haptic(); // Visual flash: briefly lighten the button setFlash(true); clearTimeout(flashRef.current); flashRef.current = setTimeout(() => setFlash(false), 120); const updated = { ...active, kicks: [...active.kicks, { time: new Date().toISOString() }] }; setActive(updated); // auto-end in goal mode when 10 kicks reached if (updated.mode === 'goal' && updated.kicks.length >= GOAL) { endSession(updated); } } function endSession(sess) { const s = sess || active; const endTime = new Date().toISOString(); const durationSec = Math.floor((new Date(endTime) - new Date(s.startTime)) / 1000); const ended = { ...s, endTime, ended: true, durationSec }; const newList = [ended, ...sessions]; setSessions(newList); saveSessions(newList); setActive(null); setElapsed(0); // Save to API (fire and forget) if (isLoggedIn() && !localStorage.getItem('alia_guest')) { saveKickSession({ kicks: ended.kicks.length, duration_sec: durationSec, mode: ended.mode, goal_reached: ended.mode === 'goal' && ended.kicks.length >= GOAL, }).catch(() => {}); } } const kickCount = active ? active.kicks.length : 0; const timeRemaining = Math.max(0, LIMIT_SECS - elapsed); const goalDone = active && active.mode === 'goal' && kickCount >= GOAL; const timeUp = active && active.mode === 'goal' && elapsed >= LIMIT_SECS; function fmtSecs(s) { const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`; return `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`; } const s = { wrap: { height:'100%', display:'flex', flexDirection:'column', overflow:'hidden' }, scroll: { flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'16px 16px', paddingBottom:'calc(20px + env(safe-area-inset-bottom))' }, inner: { display:'flex', flexDirection:'column', gap:14, maxWidth:480, margin:'0 auto' }, modeCard: { background:'var(--bg-card)', borderRadius:20, padding:'6px', display:'flex', gap:4, boxShadow:'0 2px 10px var(--shadow)' }, modeBtn: (active) => ({ flex:1, padding:'10px 0', borderRadius:16, border:'none', background: active ? 'var(--primary)' : 'transparent', color: active ? 'white' : 'var(--text-2)', fontSize:14, fontWeight:700, cursor:'pointer', fontFamily:'Nunito,sans-serif', transition:'all .2s', }), counterCard: { background:'var(--bg-card)', borderRadius:24, padding:'28px 20px', boxShadow:'0 2px 16px var(--shadow)', display:'flex', flexDirection:'column', alignItems:'center', gap:16 }, countDisplay: { fontSize:80, fontWeight:800, color:'var(--primary)', lineHeight:1 }, countLabel: { fontSize:16, color:'var(--text-2)', fontWeight:600, marginTop:-8 }, progressRow: { display:'flex', gap:8, flexWrap:'wrap', justifyContent:'center' }, dot: (filled) => ({ width:18, height:18, borderRadius:9, background: filled ? 'var(--primary)' : 'var(--primary-light)', transition:'background .2s', }), timerRow: { display:'flex', gap:24, alignItems:'center' }, timerItem: { display:'flex', flexDirection:'column', alignItems:'center', gap:2 }, timerVal: { fontSize:20, fontWeight:800, color:'var(--text)' }, timerLabel: { fontSize:11, fontWeight:600, color:'var(--text-2)', textTransform:'uppercase', letterSpacing:'0.6px' }, statusBadge: (color) => ({ background: color, color:'white', borderRadius:14, padding:'6px 18px', fontSize:14, fontWeight:700, }), tapBtn: (fl) => ({ width:180, height:180, borderRadius:90, border:'none', background: fl ? 'var(--primary-dark)' : 'var(--primary)', color:'white', fontSize:64, cursor:'pointer', fontFamily:'Nunito,sans-serif', boxShadow: fl ? '0 2px 10px rgba(232,99,122,0.25)' : '0 6px 24px rgba(232,99,122,0.4)', transform: fl ? 'scale(0.92)' : 'scale(1)', transition:'transform .1s, box-shadow .1s, background .1s', WebkitTapHighlightColor:'transparent', display:'flex', alignItems:'center', justifyContent:'center' }), hintText: { fontSize:14, color:'var(--text-2)', fontWeight:500, textAlign:'center', paddingTop:4 }, startBtn: { width:'100%', padding:'18px 0', borderRadius:18, border:'none', background:'var(--primary)', color:'white', fontSize:18, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif', boxShadow:'0 4px 16px rgba(232,99,122,0.35)', transition:'transform .15s' }, endBtn: { padding:'12px 28px', borderRadius:16, border:'2px solid var(--border)', background:'transparent', color:'var(--text-2)', fontSize:15, fontWeight:700, cursor:'pointer', fontFamily:'Nunito,sans-serif' }, histCard: { background:'var(--bg-card)', borderRadius:20, padding:'18px 20px', boxShadow:'0 2px 10px var(--shadow)' }, histLabel: { fontSize:12, fontWeight:700, color:'var(--text-2)', textTransform:'uppercase', letterSpacing:'0.8px', marginBottom:12 }, histItem: { display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 0', borderBottom:'1px solid var(--border)' }, histLeft: { display:'flex', flexDirection:'column', gap:2 }, histMode: { fontSize:12, color:'var(--text-2)', fontWeight:600 }, histKicks: { fontSize:18, fontWeight:800, color:'var(--text)' }, histRight: { display:'flex', flexDirection:'column', alignItems:'flex-end', gap:2 }, histTime: { fontSize:13, color:'var(--text-2)', fontWeight:500 }, histDur: { fontSize:13, color:'var(--primary)', fontWeight:700 }, emptyTxt: { textAlign:'center', color:'var(--text-2)', fontSize:15, fontWeight:500, padding:'12px 0' }, }; return (