From cd5b21514d0a40e18401e2f6bae22d8e980be06d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 00:28:35 +0000 Subject: [PATCH] Polish: proper HTML document, strict CSP, lazy-load three.js, a11y fixes - add doctype, lang and viewport (page was in quirks mode, 980px on phones) - move inline script to app.js; add Content-Security-Policy meta - load three.js after page load - label week slider, wire tab/tabpanel aria, fix dark-mode week numbers Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LSDgjk1Agjy9Z2UH2n8bc3 --- app.js | 539 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 570 ++--------------------------------------------------- ring3d.js | 17 ++ 3 files changed, 576 insertions(+), 550 deletions(-) create mode 100644 app.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..e9a33de --- /dev/null +++ b/app.js @@ -0,0 +1,539 @@ +(function(){ + "use strict"; + + /* ---------------- date helpers ---------------- */ + function toDate(y,m,d){ return new Date(y,m,d); } + function fromInput(str){ + if(!str) return null; + var p = str.split("-"); + return new Date(+p[0], +p[1]-1, +p[2]); + } + function toInputVal(d){ + return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); + } + function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; } + function daysBetween(a,b){ // b - a, in whole days, ignoring time + var A = new Date(a.getFullYear(),a.getMonth(),a.getDate()); + var B = new Date(b.getFullYear(),b.getMonth(),b.getDate()); + return Math.round((B-A)/86400000); + } + function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); } + function mod(n,m){ return ((n % m) + m) % m; } + var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"]; + var today = new Date(); today.setHours(0,0,0,0); + + function loadLS(key, fallback){ + try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; } + } + function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} } + + /* ---------------- mode switching ---------------- */ + var modeBtns = document.querySelectorAll(".mode-btn"); + var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") }; + modeBtns.forEach(function(btn){ + btn.addEventListener("click", function(){ + modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); }); + btn.classList.add("active"); btn.setAttribute("aria-selected","true"); + Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); }); + panels[btn.dataset.mode].classList.add("active"); + saveLS("ca-mode", btn.dataset.mode); + }); + }); + var savedMode = loadLS("ca-mode", null); + if(savedMode && panels[savedMode]){ + modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); }); + Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); }); + } + + /* ================================================================ + PHASE + OVULATION REFERENCE DATA + ================================================================ */ + var PHASES = { + menstrual: { + title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)", + window:"Cycle day 1 through the end of bleeding", + desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.", + hormone:"Estrogen & progesterone: low", energy:"Energy: lowest", + symptoms:"Cramping, fatigue, lower back ache, mood dips", + tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone." + }, + follicular: { + title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)", + window:"Cycle day 1 through ovulation (overlaps menstrual)", + desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.", + hormone:"Estrogen: rising", energy:"Energy: climbing", + symptoms:"Clearer skin, sharper focus, rising motivation", + tip:"This is the physiologically best window for starting new training blocks or demanding projects." + }, + ovulatory: { + title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)", + window:"Roughly one day, mid-cycle", + desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.", + hormone:"LH: sharp surge", energy:"Energy & libido: peak", + symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)", + tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released." + }, + luteal: { + title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)", + window:"Ovulation through the day before your next period", + desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.", + hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining", + symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)", + tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened." + } + }; + + // offset relative to ovulation day (0 = ovulation) + var OVULATION_DAYS = { + "-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."}, + "-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."}, + "-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."}, + "-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."}, + "-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."}, + "0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."}, + "1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."}, + "2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."}, + "3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."}, + "4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."}, + "5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."} + }; + var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"}; + + function phaseForDayInCycle(dic, periodLen, ovulationDic){ + if(dic < periodLen) return "menstrual"; + if(dic < ovulationDic) return "follicular"; + if(dic === ovulationDic) return "ovulatory"; + return "luteal"; + } + function isFertile(dic, ovulationDic){ + return dic >= ovulationDic - 5 && dic <= ovulationDic; + } + + /* ================================================================ + PREGNANCY WEEK DATA (1–40) + ================================================================ */ + var WEEKS = [ + {c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."}, + {c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."}, + {c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."}, + {c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."}, + {c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."}, + {c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."}, + {c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."}, + {c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."}, + {c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."}, + {c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."}, + {c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."}, + {c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."}, + {c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."}, + {c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."}, + {c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."}, + {c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."}, + {c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."}, + {c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."}, + {c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."}, + {c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."}, + {c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."}, + {c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."}, + {c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."}, + {c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."}, + {c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."}, + {c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."}, + {c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."}, + {c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."}, + {c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."}, + {c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."}, + {c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."}, + {c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."}, + {c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."}, + {c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."}, + {c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."}, + {c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."}, + {c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."}, + {c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."}, + {c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."}, + {c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."} + ]; + + /* ================================================================ + SVG helpers + ================================================================ */ + function polarToXY(cx,cy,r,angleDeg){ + var rad = (angleDeg-90) * Math.PI/180; + return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)]; + } + function arcPath(cx,cy,r,startDeg,endDeg){ + if(endDeg <= startDeg) endDeg = startDeg + 0.01; + var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg); + var large = (endDeg-startDeg) > 180 ? 1 : 0; + return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2); + } + + /* ================================================================ + SHARED STATE: period start / cycle length / period length + ================================================================ */ + var exampleStart = addDays(today, -10); + var startInput = document.getElementById("p-start"); + var cycleLenInput = document.getElementById("p-cyclelen"); + var periodLenInput = document.getElementById("p-periodlen"); + + var savedStart = loadLS("ca-start", null); + var savedCycleLen = loadLS("ca-cyclelen", null); + var savedPeriodLen = loadLS("ca-periodlen", null); + var usingExample = !savedStart; + + startInput.value = savedStart || toInputVal(exampleStart); + cycleLenInput.value = savedCycleLen || 28; + periodLenInput.value = savedPeriodLen || 5; + + function getCycleState(){ + return { + start: fromInput(startInput.value) || exampleStart, + cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)), + periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5)) + }; + } + + var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); + var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); + var pSelectedDate = new Date(today); + var oSelectedDate = new Date(today); + + [startInput,cycleLenInput,periodLenInput].forEach(function(inp){ + inp.addEventListener("input", function(){ + saveLS("ca-start", startInput.value); + saveLS("ca-cyclelen", cycleLenInput.value); + saveLS("ca-periodlen", periodLenInput.value); + usingExample = false; + document.getElementById("p-example-note").style.display = "none"; + document.getElementById("pr-example-note").style.display = "none"; + renderAll(); + }); + }); + if(!usingExample) document.getElementById("p-example-note").style.display = "none"; + + /* ================================================================ + CALENDAR RENDERING (shared builder) + ================================================================ */ + function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){ + gridEl.innerHTML = ""; + var year = monthDate.getFullYear(), month = monthDate.getMonth(); + var firstDow = new Date(year,month,1).getDay(); + var daysInMonth = new Date(year,month+1,0).getDate(); + var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7; + for(var i=0;i daysInMonth){ + cell.classList.add("pad"); + gridEl.appendChild(cell); + continue; + } + var d = new Date(year,month,dayNum); + var info = dayClassifier(d); + cell.innerHTML = ""+dayNum+""; + if(info.period) cell.classList.add("is-period"); + if(info.fertile) cell.classList.add("is-fertile"); + if(info.ovulation) cell.classList.add("is-ovulation"); + if(sameDay(d,today)) cell.classList.add("today"); + if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected"); + cell.setAttribute("aria-label", d.toDateString()); + cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d)); + gridEl.appendChild(cell); + } + } + + function classifyForCycle(d, state){ + var dic = mod(daysBetween(state.start, d), state.cycleLen); + var ovDic = mod(state.cycleLen - 14, state.cycleLen); + return { + period: dic < state.periodLen, + fertile: isFertile(dic, ovDic), + ovulation: dic === ovDic, + dic: dic, ovDic: ovDic + }; + } + + /* ---------------- PERIOD tab render ---------------- */ + function renderPeriodCalendar(){ + var state = getCycleState(); + document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear(); + buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth, + function(d){ return classifyForCycle(d, state); }, + function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); }, + pSelectedDate + ); + } + + function renderPeriodDetail(){ + var state = getCycleState(); + var info = classifyForCycle(pSelectedDate, state); + var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic); + var phase = PHASES[phaseKey]; + var el = document.getElementById("p-detail"); + el.innerHTML = + ''+pSelectedDate.toDateString()+''+ + ''+phase.title+' phase'+(info.fertile?' · fertile window':'')+''+ + '
Cycle day '+(info.dic+1)+'
'+ + '

'+phase.desc+'

'+ + '
'+ + '
Hormones
'+phase.hormone+'
'+ + '
Typical symptoms
'+phase.symptoms+'
'+ + '
'+ + '

Care note — '+phase.tip+'

'; + } + + function renderPeriodStats(){ + var state = getCycleState(); + var dic = mod(daysBetween(state.start, today), state.cycleLen); + var ovDic = mod(state.cycleLen - 14, state.cycleLen); + var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen); + if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen; + var daysToOvulation = mod(ovDic - dic, state.cycleLen); + var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic); + document.getElementById("period-stats").innerHTML = + statTile(dic+1, "Cycle day today") + + statTile(PHASES[phaseKey].title, "Current phase") + + statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") + + statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation"); + + // hero ring + var cx=100, cy=100, r=88; + var perStart=0, perEnd = state.periodLen/state.cycleLen*360; + var follEnd = ovDic/state.cycleLen*360; + var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360; + var lutStart = (ovDic+1)/state.cycleLen*360; + document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd)); + document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart)); + document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen))); + document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360)); + var todayAngle = dic/state.cycleLen*360; + var tp = polarToXY(cx,cy,r,todayAngle); + document.getElementById("today-marker").setAttribute("cx", tp[0]); + document.getElementById("today-marker").setAttribute("cy", tp[1]); + document.getElementById("cycle-day-big").textContent = dic+1; + // hand the same numbers to the optional 3D ring (ring3d.js) + window.cycleRingData = {perEnd:perEnd, fertStart:fertStart, fertEnd:follEnd+(360/state.cycleLen), + lutStart:lutStart, todayAngle:todayAngle}; + window.dispatchEvent(new CustomEvent("cycle:ring", {detail: window.cycleRingData})); + } + + function statTile(num,label){ + return '
'+num+'
'+label+'
'; + } + + document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); }); + document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); }); + + function renderPhaseRefGrid(){ + var order = ["menstrual","follicular","ovulatory","luteal"]; + document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){ + var p = PHASES[key]; + return '
0'+(i+1)+'

'+p.title+'

'+ + '

'+p.desc+'

'+ + '
'+p.window+'
'; + }).join(""); + } + + /* ---------------- OVULATION tab render ---------------- */ + var ovSelectedOffset = 0; + function renderOvTimeline(){ + var wrap = document.getElementById("ov-timeline"); + wrap.innerHTML = ""; + for(var off=-5; off<=5; off++){ + var d = OVULATION_DAYS[String(off)]; + var btn = document.createElement("button"); + btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":""); + btn.innerHTML = '
'+(off===0?"OVUL.":(off>0?"+":"")+off)+'
'+ + '
'; + btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off)); + wrap.appendChild(btn); + } + } + function renderOvDetailFromOffset(){ + var d = OVULATION_DAYS[String(ovSelectedOffset)]; + var el = document.getElementById("o-detail"); + var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation"); + el.innerHTML = + ''+label+''+ + 'Fertility: '+d.fert+''+ + '
'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'
'+ + '
'+ + '
Basal body temp
'+d.bbt+'
'+ + '
Cervical mucus
'+d.mucus+'
'+ + '
'+ + '

'+d.note+'

'; + } + + function renderOvulationCalendar(){ + var state = getCycleState(); + document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear(); + buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth, + function(d){ return classifyForCycle(d, state); }, + function(d){ + oSelectedDate = d; + var info = classifyForCycle(d, state); + var offset = info.dic - info.ovDic; + if(offset < -5) offset = -5; if(offset > 5) offset = 5; + ovSelectedOffset = offset; + renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar(); + }, + oSelectedDate + ); + } + + function renderOvulationStats(){ + var state = getCycleState(); + var dic = mod(daysBetween(state.start, today), state.cycleLen); + var ovDic = mod(state.cycleLen - 14, state.cycleLen); + var daysToOvulation = mod(ovDic - dic, state.cycleLen); + var fertileStartDic = mod(ovDic-5, state.cycleLen); + var inFertile = isFertile(dic, ovDic); + var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen); + document.getElementById("ovulation-stats").innerHTML = + statTile(inFertile ? "Yes" : "No", "In fertile window") + + statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") + + statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") + + statTile("6 days", "Window length"); + } + + document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); }); + document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); }); + + /* ================================================================ + PREGNANCY tab + ================================================================ */ + var lmpInput = document.getElementById("pr-lmp"); + var exampleLMP = addDays(today, -20*7 - 3); + var savedLMP = loadLS("ca-lmp", null); + lmpInput.value = savedLMP || toInputVal(exampleLMP); + if(savedLMP) document.getElementById("pr-example-note").style.display = "none"; + + var slider = document.getElementById("week-slider"); + var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); + + lmpInput.addEventListener("input", function(){ + saveLS("ca-lmp", lmpInput.value); + document.getElementById("pr-example-note").style.display = "none"; + document.getElementById("p-example-note").style.display = "none"; + syncSliderToLMP(); + renderPregnancy(); + }); + + function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; } + function currentGestWeek(){ + var lmp = getLMP(); + var w = Math.floor(daysBetween(lmp,today)/7) + 1; + return Math.max(1, Math.min(40, w)); + } + function syncSliderToLMP(){ slider.value = currentGestWeek(); } + syncSliderToLMP(); + + function renderPregnancyStats(){ + var lmp = getLMP(); + var due = addDays(lmp, 280); + var week = currentGestWeek(); + var daysLeft = Math.max(0, daysBetween(today, due)); + document.getElementById("pregnancy-stats").innerHTML = + statTile(week, "Weeks along") + + statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") + + statTile(daysLeft, "Days to due date") + + statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date"); + + var pct = (week-1)/39; + var pathLen = 230; + var offset = pathLen - pct*pathLen; + var path = document.getElementById("pregnancy-progress-path"); + path.setAttribute("stroke-dashoffset", offset.toFixed(1)); + try{ + var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen; + var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null; + if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); } + }catch(e){} + } + + function renderWeekPanel(week){ + var w = WEEKS[week-1]; + document.getElementById("wk-badge").textContent = "WEEK " + week; + document.getElementById("wk-compare").textContent = "size of " + w.c; + document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m; + document.getElementById("wk-dev").textContent = w.dev; + document.getElementById("wk-body").textContent = w.body; + document.getElementById("wk-tip").textContent = w.tip; + var tri = week<=13?1:(week<=27?2:3); + var triColor = "var(--pregnancy)"; + document.getElementById("wk-blob").innerHTML = + ''; + document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); }); + var active = document.querySelector('.film-item[data-week="'+week+'"]'); + if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"}); + } + + slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); }); + + function buildFilmstrip(){ + var wrap = document.getElementById("week-filmstrip"); + wrap.innerHTML = ""; + for(var w=1; w<=40; w++){ + var item = document.createElement("button"); + item.className = "film-item"; + item.dataset.week = w; + item.innerHTML = '
WK
'+w+'
'; + item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w)); + wrap.appendChild(item); + } + } + + function renderPregnancyCalendar(){ + var lmp = getLMP(); + document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear(); + var grid = document.getElementById("pr-daygrid"); + grid.innerHTML = ""; + var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth(); + var firstDow = new Date(year,month,1).getDay(); + var daysInMonth = new Date(year,month+1,0).getDate(); + var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7; + for(var i=0;idaysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; } + var d = new Date(year,month,dayNum); + var wk = Math.floor(daysBetween(lmp,d)/7)+1; + var inRange = wk>=1 && wk<=42; + cell.innerHTML = ""+dayNum+"" + (inRange ? 'W'+wk+'' : ""); + if(inRange && wk<=40) cell.classList.add("is-fertile"); + if(sameDay(d,today)) cell.classList.add("today"); + cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk)); + grid.appendChild(cell); + } + } + document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); }); + document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); }); + + function renderPregnancy(){ + renderPregnancyStats(); + renderWeekPanel(currentGestWeek()); + renderPregnancyCalendar(); + } + + /* ================================================================ + INIT + ================================================================ */ + function renderAll(){ + renderPeriodStats(); + renderPeriodCalendar(); + renderPeriodDetail(); + renderOvulationStats(); + renderOvTimeline(); + renderOvDetailFromOffset(); + renderOvulationCalendar(); + renderPregnancy(); + } + + renderPhaseRefGrid(); + buildFilmstrip(); + slider.value = currentGestWeek(); + renderAll(); +})(); diff --git a/index.html b/index.html index 19c7a6e..1498e35 100644 --- a/index.html +++ b/index.html @@ -1,3 +1,9 @@ + + + + + + CycleWise AU @@ -131,6 +137,7 @@ button{font-family:inherit;} .mode-panel.active{display:block;animation:rise .4s ease both;} @keyframes rise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}} @media (prefers-reduced-motion: reduce){.mode-panel.active{animation:none;}} +@media (prefers-color-scheme: dark){.mode-btn[data-mode="pregnancy"].active{color:#1B1417;}} .hero{ display:grid;grid-template-columns:1.3fr 1fr;gap:40px; @@ -301,6 +308,7 @@ input[type="range"].week-slider::-moz-range-thumb{ .filmstrip{display:flex;gap:8px;overflow-x:auto;padding:4px 2px 20px;margin-top:8px;} .film-item{ + color:var(--ink); flex:none;width:64px;height:64px;border-radius:12px;border:1px solid var(--line); background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center; cursor:pointer;font-family:"IBM Plex Mono",monospace; @@ -314,7 +322,8 @@ footer.note{ font-size:0.78rem;color:var(--ink-soft);line-height:1.6; } - + +
@@ -328,15 +337,15 @@ footer.note{ Menstrual & Pregnancy Charting
-
+
01 — Menstrual Cycle @@ -402,7 +411,7 @@ footer.note{
-
+
02 — Fertile Window @@ -450,7 +459,7 @@ footer.note{
-
+
03 — Forty Weeks @@ -480,7 +489,7 @@ footer.note{ Trimester 2 · Weeks 14–27 Trimester 3 · Weeks 28–40
- +
Wk 1Wk 13Wk 27Wk 40
@@ -528,550 +537,11 @@ footer.note{
- + - + + diff --git a/ring3d.js b/ring3d.js index 425ec82..a789577 100644 --- a/ring3d.js +++ b/ring3d.js @@ -4,6 +4,7 @@ If WebGL is missing or anything fails, the flat ring stays visible. */ (function(){ "use strict"; + function start(){ try { if (!window.THREE) return; var art = document.getElementById("phase-arc-menstrual"); @@ -131,4 +132,20 @@ var h = document.querySelector(".hero-art.has-3d"); if (h) h.classList.remove("has-3d"); if (window.console) console.warn("3D ring disabled:", err && err.message); } + } + + // three.js is big: fetch it only after the page is up, so the page shows first. + function loadThree(){ + var tag = document.createElement("script"); + tag.src = "vendor/three.min.js"; + tag.onload = start; + document.head.appendChild(tag); + } + function schedule(){ + if (window.THREE) { start(); } + else if (window.requestIdleCallback) { requestIdleCallback(loadThree, {timeout: 2000}); } + else { setTimeout(loadThree, 300); } + } + if (document.readyState === "complete") schedule(); + else window.addEventListener("load", schedule); })();