// Calendar.jsx — cycle calendar with phases, ovulation, predictions and memorable days (function () { const { useState, useMemo, useRef, useEffect, useCallback } = React; const { t, cycleMarksForRange, cycleMarksFromPeriods, currentCycleInfo, calculateCycle, formatDayMonth, PHASE_INFO, localISO, pluralDayRu } = window.AliaData; const PHASE_COLOR = { period:'#E8637A', fertile:'#16A8A8', ovulation:'#16A8A8', follicular:'#7FA8E0', luteal:'#C98BD4', delay:'#E8923D' }; function phaseLabel(lang) { const ru = lang === 'ru'; return { period: ru ? 'Менструация' : 'Period', follicular: ru ? 'Фолликулярная фаза' : 'Follicular phase', fertile: ru ? 'Фертильное окно' : 'Fertile window', ovulation: ru ? 'Овуляция' : 'Ovulation', luteal: ru ? 'Лютеиновая фаза' : 'Luteal phase', delay: ru ? 'Задержка' : 'Delay', }; } const C = { period: '#E8637A', periodTxt: '#FFFFFF', fertile: '#16A8A8', ovulation: '#16A8A8', delay: '#E8923D', }; const iso = d => localISO(d); const startOfMonth = (y, m) => new Date(y, m, 1); const daysInMonth = (y, m) => new Date(y, m + 1, 0).getDate(); // Monday-first index for a Date const mondayIndex = d => (d.getDay() + 6) % 7; function todayISO() { const d = new Date(); d.setHours(0, 0, 0, 0); return iso(d); } // ── Symptom / mood / flow / event option tables ──────────────── const FLOWS = [['light','💧'], ['medium','💧💧'], ['heavy','💧💧💧']]; const MOODS = [['happy','😊'], ['calm','😌'], ['sad','😢'], ['anxious','😰'], ['irritable','😣']]; const SYMS = [['cramps','🤕'], ['headache','🤯'], ['tender','🫶'], ['bloating','🎈'], ['fatigue','😴'], ['acne','🩹']]; const EVENTS = [['intimacy','❤️'], ['test','🧪'], ['doctor','🩺'], ['pill','💊'], ['anniversary','⭐'], ['note','📝']]; function capFirst(k) { return k.charAt(0).toUpperCase() + k.slice(1); } function flowLabel(lang, k) { return t('flow' + capFirst(k), lang); } function moodLabel(lang, k) { return t('mood' + capFirst(k), lang); } function symLabel(lang, k) { return t('sym' + capFirst(k), lang); } function evtLabel(lang, k) { return t('evt' + capFirst(k), lang); } // payload helper — does this day have any user mark? function hasMark(p) { if (!p) return false; return !!(p.flow || p.mood || (p.symptoms && p.symptoms.length) || (p.events && p.events.length)); } /* ── Phase info bottom sheet ───────────────────────────────────── */ function PhaseInfoSheet({ lang, phase, onClose }) { const info = (PHASE_INFO[lang] || PHASE_INFO.ru)[phase]; if (!info) return null; const color = PHASE_COLOR[phase] || 'var(--primary)'; const s = { overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', zIndex:400, display:'flex', alignItems:'flex-end', backdropFilter:'blur(4px)' }, sheet: { width:'100%', background:'var(--bg-card)', borderRadius:'24px 24px 0 0', padding:'18px 24px', paddingBottom:'calc(26px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:14, boxShadow:'0 -4px 24px rgba(0,0,0,0.15)', maxWidth:520, margin:'0 auto', maxHeight:'82vh', overflowY:'auto' }, handle: { width:40, height:4, borderRadius:2, background:'var(--border)', margin:'0 auto' }, head: { display:'flex', alignItems:'center', gap:12 }, badge: { width:48, height:48, borderRadius:14, display:'flex', alignItems:'center', justifyContent:'center', fontSize:24, background: color + '22', flexShrink:0 }, title: { fontSize:19, fontWeight:800, color: color }, text: { fontSize:15, lineHeight:1.55, color:'var(--text)', fontWeight:500 }, close: { padding:'14px 0', borderRadius:14, border:'none', background:'var(--primary-light)', color:'var(--primary)', fontSize:15, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif', marginTop:2 }, }; return (
e.stopPropagation()}>
{info.emoji}
{info.title}
{info.text}
); } /* ── Bottom sheet for a single day ─────────────────────────────── */ function DaySheet({ lang, dateISO, mark, payload, onSave, onClose, onPhaseInfo }) { const ru = lang === 'ru'; const [flow, setFlow] = useState(payload.flow || ''); const [mood, setMood] = useState(payload.mood || ''); const [syms, setSyms] = useState(payload.symptoms || []); const [events, setEvents] = useState(payload.events || []); const toggle = (arr, set, v) => set(arr.includes(v) ? arr.filter(x => x !== v) : [...arr, v]); const d = new Date(dateISO + 'T00:00:00'); const dateLabel = d.getDate() + ' ' + t('calMonths', lang)[d.getMonth()].toLowerCase(); const s = { overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', zIndex:300, display:'flex', alignItems:'flex-end', backdropFilter:'blur(4px)' }, sheet: { width:'100%', background:'var(--bg-card)', borderRadius:'24px 24px 0 0', padding:'18px 22px', paddingBottom:'calc(24px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:14, boxShadow:'0 -4px 24px rgba(0,0,0,0.15)', maxWidth:520, margin:'0 auto', maxHeight:'82vh', overflowY:'auto' }, handle: { width:40, height:4, borderRadius:2, background:'var(--border)', margin:'0 auto' }, title: { fontSize:18, fontWeight:800, color:'var(--text)', textTransform:'capitalize' }, cdsub: { fontSize:13, color:'var(--text-2)', fontWeight:600 }, lbl: { fontSize:11, fontWeight:700, color:'var(--text-2)', letterSpacing:'0.5px', textTransform:'uppercase', marginBottom:9, display:'block' }, chips: { display:'flex', flexWrap:'wrap', gap:8 }, chip: (a) => ({ display:'flex', alignItems:'center', gap:6, padding:'9px 13px', borderRadius:13, border:'2px solid', borderColor: a ? 'var(--primary)' : 'var(--border)', background: a ? 'var(--primary-light)' : 'var(--bg)', color: a ? 'var(--primary)' : 'var(--text)', fontSize:13, fontWeight:700, cursor:'pointer', fontFamily:'Nunito,sans-serif' }), save: { padding:'15px 0', borderRadius:15, border:'none', background:'var(--primary)', color:'#fff', fontSize:16, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif', marginTop:4 }, }; return (
e.stopPropagation()}>
{dateLabel}
{mark && mark.phase && ( )}
{t('dayFlow', lang)}
{FLOWS.map(([k, e]) => ( ))}
{t('dayMood', lang)}
{MOODS.map(([k, e]) => ( ))}
{t('daySymptoms', lang)}
{SYMS.map(([k, e]) => ( ))}
{t('dayEvents', lang)}
{EVENTS.map(([k, e]) => ( ))}
); } /* ── A single month grid ───────────────────────────────────────── */ function MonthGrid({ lang, year, month, marks, logs, today, onDayTap }) { const total = daysInMonth(year, month); const lead = mondayIndex(startOfMonth(year, month)); const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let day = 1; day <= total; day++) cells.push(day); const st = { wrap: { padding:'4px 0 18px' }, title: { fontSize:17, fontWeight:800, color:'var(--text)', textAlign:'center', margin:'10px 0 12px' }, grid: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:10 }, cellWrap: { minHeight:46, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'flex-start', position:'relative' }, cd: { fontSize:9, fontWeight:700, color:'var(--text-2)', height:11, lineHeight:'11px', marginBottom:1 }, }; function renderDay(day) { const d = new Date(year, month, day); const key = iso(d); const m = marks[key]; const p = logs[key]; const isToday = key === today; const marked = hasMark(p); // base cell styles — большая цифра = ДАТА, маленькая сверху = день цикла let circle = { width:34, height:34, borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, fontWeight:700, color:'var(--text)', cursor:'pointer', position:'relative', border:'2px solid transparent', boxSizing:'border-box', lineHeight:1, }; if (m && m.phase === 'delay') { circle.background = C.delay; circle.color = '#fff'; circle.fontWeight = 800; } else if (m && m.phase === 'period') { if (m.predicted) { circle.border = '2px dashed ' + C.period; circle.color = C.period; } else { circle.background = C.period; circle.color = C.periodTxt; } } else if (m && m.isOvulation) { circle.border = '2px dashed ' + C.ovulation; circle.color = C.ovulation; circle.fontWeight = 800; } else if (m && m.phase === 'fertile') { circle.color = C.fertile; circle.fontWeight = 800; } if (isToday) { circle.boxShadow = '0 0 0 2px var(--bg), 0 0 0 4px var(--text)'; } return (
onDayTap(key, m, p || {})}>
{m && m.cycleDay ? m.cycleDay : ''}
{day}
{marked &&
}
); } return (
{t('calMonths', lang)[month] + ', ' + year}
{cells.map((c, idx) => c === null ?
: renderDay(c))}
); } /* ── Year overview (12 mini months) ────────────────────────────── */ function YearGrid({ lang, year, marks, today, onMonthTap }) { const st = { grid: { display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:14, padding:'8px 0 18px' }, mini: { cursor:'pointer', background:'var(--bg-card)', borderRadius:14, padding:'10px 8px', boxShadow:'0 2px 10px var(--shadow)' }, mt: { fontSize:12, fontWeight:800, color:'var(--text)', textAlign:'center', marginBottom:6 }, mg: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:2 }, dot: (bg, brd) => ({ width:11, height:11, margin:'0 auto', borderRadius:'50%', fontSize:7, lineHeight:'11px', textAlign:'center', background:bg || 'transparent', border: brd || 'none', color:'#fff', boxSizing:'border-box' }), }; function miniMonth(month) { const total = daysInMonth(year, month); const lead = mondayIndex(startOfMonth(year, month)); const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let day = 1; day <= total; day++) cells.push(day); return (
onMonthTap(month)}>
{t('calMonths', lang)[month]}
{cells.map((c, idx) => { if (c === null) return
; const key = iso(new Date(year, month, c)); const m = marks[key]; let bg = 'transparent', brd = 'none', color = 'var(--text-2)'; if (m && m.phase === 'delay') { bg = C.delay; color = '#fff'; } else if (m && m.phase === 'period') { if (m.predicted) { brd = '1px dashed ' + C.period; color = C.period; } else { bg = C.period; color = '#fff'; } } else if (m && (m.isOvulation || m.phase === 'fertile')) { brd = '1px solid ' + C.fertile; color = C.fertile; } return
{c}
; })}
); } return
{[0,1,2,3,4,5,6,7,8,9,10,11].map(miniMonth)}
; } /* ── Calendar screen ───────────────────────────────────────────── */ function CalendarScreen({ lang, cycle, periods, logs, onSaveLog, onEditPeriod, onClose }) { const ru = lang === 'ru'; const today = todayISO(); const td = new Date(today + 'T00:00:00'); const [view, setView] = useState('month'); // 'month' | 'year' const [year, setYear] = useState(td.getFullYear()); const [daySheet, setDaySheet] = useState(null); // { dateISO, mark, payload } const [phaseSheet, setPhaseSheet] = useState(null); // phase key const info = useMemo(() => currentCycleInfo(periods, cycle), [periods, cycle]); const labels = phaseLabel(lang); const scrollRef = useRef(null); const todayRef = useRef(null); // Range of months to render: 6 back .. 12 forward const months = useMemo(() => { const list = []; const base = new Date(td.getFullYear(), td.getMonth(), 1); for (let i = -6; i <= 12; i++) { const d = new Date(base.getFullYear(), base.getMonth() + i, 1); list.push({ y: d.getFullYear(), m: d.getMonth() }); } return list; }, [today]); // eslint-disable-line const marks = useMemo(() => { const first = months[0]; const last = months[months.length - 1]; const fromISO = iso(new Date(first.y, first.m, 1)); const toISO = iso(new Date(last.y, last.m, daysInMonth(last.y, last.m))); return cycleMarksFromPeriods(periods, cycle, fromISO, toISO); }, [months, cycle, periods]); // year-view marks (full selected year) const yearMarks = useMemo(() => { return cycleMarksFromPeriods(periods, cycle, year + '-01-01', year + '-12-31'); }, [year, cycle, periods]); const scrollToToday = useCallback(() => { if (todayRef.current && scrollRef.current) { todayRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, []); useEffect(() => { // jump to current month on first open (no animation) if (view === 'month' && todayRef.current) { todayRef.current.scrollIntoView({ block: 'center' }); } }, [view]); // eslint-disable-line const handleDayTap = useCallback((dateISO, m, payload) => { setDaySheet({ dateISO, mark: m, payload }); }, []); const handleSave = useCallback((dateISO, payload) => { onSaveLog(dateISO, payload); setDaySheet(null); }, [onSaveLog]); const st = { root: { position:'fixed', inset:0, zIndex:250, background:'var(--bg)', display:'flex', flexDirection:'column', fontFamily:'Nunito,sans-serif' }, header: { display:'flex', alignItems:'center', justifyContent:'space-between', gap:10, padding:'12px 16px', paddingTop:'calc(12px + env(safe-area-inset-top))', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 }, close: { background:'var(--bg)', border:'none', borderRadius:10, width:38, height:38, fontSize:20, cursor:'pointer', color:'var(--text)' }, toggle: { display:'flex', background:'var(--bg)', borderRadius:11, padding:3, gap:2 }, tBtn: (a) => ({ padding:'7px 16px', borderRadius:9, border:'none', cursor:'pointer', fontFamily:'Nunito,sans-serif', fontSize:13, fontWeight:800, background: a ? 'var(--bg-card)' : 'transparent', color: a ? 'var(--primary)' : 'var(--text-2)', boxShadow: a ? '0 1px 4px var(--shadow)' : 'none' }), legend: { display:'flex', flexWrap:'wrap', gap:'6px 14px', padding:'10px 16px', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 }, legItem: { display:'flex', alignItems:'center', gap:6, fontSize:11, fontWeight:600, color:'var(--text-2)' }, sw: (bg, brd) => ({ width:14, height:14, borderRadius:'50%', background:bg, border:brd || 'none', flexShrink:0 }), scroll: { flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'0 16px 16px' }, yearNav: { display:'flex', alignItems:'center', justifyContent:'center', gap:24, padding:'12px 0 4px' }, yearBtn: { background:'none', border:'none', fontSize:22, cursor:'pointer', color:'var(--primary)', fontWeight:900 }, yearLbl: { fontSize:18, fontWeight:800, color:'var(--text)', minWidth:70, textAlign:'center' }, todayFab: { position:'absolute', left:'50%', transform:'translateX(-50%)', bottom:'calc(86px + env(safe-area-inset-bottom))', background:'var(--bg-card)', border:'1px solid var(--border)', borderRadius:22, padding:'10px 18px', fontSize:13, fontWeight:800, color:'var(--text)', cursor:'pointer', fontFamily:'Nunito,sans-serif', boxShadow:'0 4px 16px var(--shadow)', zIndex:5 }, footer: { flexShrink:0, padding:'12px 16px calc(14px + env(safe-area-inset-bottom))', background:'var(--bg-card)', borderTop:'1px solid var(--border)' }, editBtn: { width:'100%', padding:'15px 0', borderRadius:15, border:'none', background:'var(--primary)', color:'#fff', fontSize:15, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif' }, weekdays: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', padding:'6px 0', position:'sticky', top:0, background:'var(--bg)', zIndex:2 }, wd: { textAlign:'center', fontSize:11, fontWeight:700, color:'var(--text-2)' }, summary: { background:'var(--bg-card)', borderRadius:18, padding:'16px 18px', margin:'14px 0 6px', boxShadow:'0 2px 12px var(--shadow)', display:'flex', flexDirection:'column', gap:12 }, sumTop: { display:'flex', alignItems:'center', justifyContent:'space-between', gap:12 }, sumDay: { fontSize:13, color:'var(--text-2)', fontWeight:600 }, sumDayBig: { fontSize:26, fontWeight:900, color:'var(--text)', lineHeight:1.1 }, sumPhase: (c) => ({ display:'inline-flex', alignItems:'center', gap:6, padding:'7px 13px', borderRadius:13, border:'none', cursor:'pointer', fontFamily:'Nunito,sans-serif', fontSize:13, fontWeight:800, background: c + '22', color: c }), bar: { height:8, borderRadius:5, background:'var(--primary-light)', overflow:'hidden' }, barFill: (pct, c) => ({ height:'100%', width: Math.max(4, Math.min(100, pct)) + '%', background:c, borderRadius:5, transition:'width .3s' }), sumRow: { display:'flex', justifyContent:'space-between', fontSize:13 }, hint: { fontSize:11, color:'var(--text-2)', textAlign:'center', fontWeight:600 }, }; const phaseC = info ? (PHASE_COLOR[info.phase] || 'var(--primary)') : 'var(--primary)'; function ovulationLine() { if (!info) return null; if (info.isOvulationToday) return '🌟 ' + t('calOvulationToday', lang); if (info.daysToOvulation > 0) return t('calOvulationIn', lang) + ': ' + info.daysToOvulation + ' ' + t('days', lang); return t('calOvulationDone', lang); } const summaryCard = info ? (
{t('dayCycleDay', lang)}
{info.cycleDay} {t('calCycleDayOf', lang)} {info.cycleLength}
{info.delayDays > 0 ? (
{ru ? `Задержка ${info.delayDays} ${pluralDayRu(info.delayDays)}` : `Period late by ${info.delayDays} ${info.delayDays === 1 ? 'day' : 'days'}`}
) : (
{t('calNextPeriodIn', lang)} {info.daysToNextPeriod} {t('days', lang)} · {formatDayMonth(info.nextPeriod, lang)}
)}
{ovulationLine()} {formatDayMonth(info.ovulation, lang)}
{t('calTapPhase', lang)}
) : null; return (
{t('calLegPeriod', lang)} {t('calLegFertile', lang)} {t('calLegOvulation', lang)} {t('calLegPredicted', lang)} {t('calLegMark', lang)}
{view === 'month' ? (
{summaryCard}
{t('calWeekdays', lang).map((w, i) =>
{w}
)}
{months.map(({ y, m }) => { const isCurrent = y === td.getFullYear() && m === td.getMonth(); return (
); })}
) : (
{year}
{ setView('month'); }} />
)} {view === 'month' && ( )}
{daySheet && ( setDaySheet(null)} onPhaseInfo={ph => setPhaseSheet(ph)} /> )} {phaseSheet && ( setPhaseSheet(null)} /> )}
); } /* ── Period editor (tap-to-toggle menstruation days) ───────────── */ function expandPeriodsToSet(periods, profile) { const set = {}; const periodLen = (profile && profile.period_length) || 5; // Не предзаполняем будущие дни: для периода без даты окончания разворачиваем // только до сегодня. Иначе текущий период «без конца» помечал бы 4 будущих дня, // и при снятии сегодняшнего дня они оставались бы как месячные на будущее. const today = new Date(todayISO() + 'T00:00:00'); (periods || []).forEach(p => { const s = (typeof p === 'string') ? p : (p && (p.start_date || p.start)); if (!s) return; const sd = new Date(s + 'T00:00:00'); if (isNaN(sd.getTime())) return; let endd; if (p && p.end_date) { endd = new Date(p.end_date + 'T00:00:00'); } else { endd = new Date(sd.getTime() + (periodLen - 1) * 86400000); } if (endd.getTime() > today.getTime()) endd = today; for (let d = new Date(sd); d.getTime() <= endd.getTime(); d = new Date(d.getTime() + 86400000)) { set[iso(d)] = true; } }); return set; } function setToEpisodes(set) { const dates = Object.keys(set).filter(k => set[k]).sort(); const episodes = []; let cur = null; dates.forEach(k => { const d = new Date(k + 'T00:00:00'); if (cur && (d.getTime() - new Date(cur.end + 'T00:00:00').getTime()) === 86400000) { cur.end = k; } else { cur = { start: k, end: k }; episodes.push(cur); } }); return episodes.map(e => ({ start_date: e.start, end_date: e.end })); } function PeriodEditScreen({ lang, periods, cycle, onSave, onClose }) { const today = todayISO(); const td = new Date(today + 'T00:00:00'); const [sel, setSel] = useState(() => expandPeriodsToSet(periods, cycle)); const todayRef = useRef(null); const months = useMemo(() => { const list = []; const base = new Date(td.getFullYear(), td.getMonth(), 1); for (let i = -12; i <= 2; i++) { const d = new Date(base.getFullYear(), base.getMonth() + i, 1); list.push({ y: d.getFullYear(), m: d.getMonth() }); } return list; }, [today]); // eslint-disable-line useEffect(() => { if (todayRef.current) todayRef.current.scrollIntoView({ block: 'center' }); }, []); const toggle = key => setSel(s => { const n = { ...s }; if (n[key]) delete n[key]; else n[key] = true; return n; }); const st = { root: { position:'fixed', inset:0, zIndex:260, background:'var(--bg)', display:'flex', flexDirection:'column', fontFamily:'Nunito,sans-serif' }, header: { display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 18px', paddingTop:'calc(12px + env(safe-area-inset-top))', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 }, cancel: { background:'none', border:'none', fontSize:15, fontWeight:700, color:'var(--text-2)', cursor:'pointer', fontFamily:'Nunito,sans-serif' }, title: { fontSize:16, fontWeight:800, color:'var(--text)' }, save: { background:'none', border:'none', fontSize:15, fontWeight:800, color:'var(--primary)', cursor:'pointer', fontFamily:'Nunito,sans-serif' }, hint: { fontSize:12, color:'var(--text-2)', fontWeight:600, textAlign:'center', padding:'10px 16px', background:'var(--bg-card)', borderBottom:'1px solid var(--border)' }, scroll: { flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'0 16px 24px' }, weekdays: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', padding:'6px 0', position:'sticky', top:0, background:'var(--bg)', zIndex:2 }, wd: { textAlign:'center', fontSize:11, fontWeight:700, color:'var(--text-2)' }, mTitle: { fontSize:16, fontWeight:800, color:'var(--text)', textAlign:'center', margin:'12px 0 10px' }, grid: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:6 }, cell: { aspectRatio:'1 / 1', display:'flex', alignItems:'center', justifyContent:'center', position:'relative' }, }; function dayCircle(on, isToday) { return { width:36, height:36, borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, fontWeight:700, cursor:'pointer', boxSizing:'border-box', lineHeight:1, background: on ? C.period : 'transparent', color: on ? '#fff' : 'var(--text)', border: on ? 'none' : '2px solid var(--border)', boxShadow: isToday ? '0 0 0 2px var(--bg), 0 0 0 4px var(--text)' : 'none', }; } function monthGrid(y, m) { const total = daysInMonth(y, m); const lead = mondayIndex(startOfMonth(y, m)); const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let day = 1; day <= total; day++) cells.push(day); const isCurrent = y === td.getFullYear() && m === td.getMonth(); return (
{t('calMonths', lang)[m] + ', ' + y}
{cells.map((c, idx) => { if (c === null) return
; const key = iso(new Date(y, m, c)); const future = new Date(y, m, c).getTime() > td.getTime(); return (
{ if (!future) toggle(key); }}>{c}
); })}
); } return (
{t('calEditPeriod', lang)}
{lang === 'ru' ? 'Нажмите на дни, когда шли месячные' : 'Tap the days you had your period'}
{t('calWeekdays', lang).map((w, i) =>
{w}
)}
{months.map(({ y, m }) => monthGrid(y, m))}
); } Object.assign(window, { CalendarScreen, PhaseInfoSheet, PeriodEditScreen }); })();