Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CycleSync | Period Tracker & Fertility Calendar</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .gradient-bg { | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| } | |
| .fertile-window { | |
| background-color: rgba(255, 105, 180, 0.2); | |
| } | |
| .period-day { | |
| background-color: rgba(255, 0, 0, 0.2); | |
| } | |
| .ovulation-day { | |
| background-color: rgba(255, 215, 0, 0.3); | |
| } | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: #c1c1c1; | |
| border-radius: 3px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: #a8a8a8; | |
| } | |
| .symptom-icon { | |
| transition: all 0.3s ease; | |
| } | |
| .symptom-icon:hover { | |
| transform: scale(1.1); | |
| } | |
| </style> | |
| </head> | |
| <body class="gradient-bg min-h-screen font-sans"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="flex flex-col md:flex-row justify-between items-center mb-8"> | |
| <div class="flex items-center mb-4 md:mb-0"> | |
| <i class="fas fa-venus text-pink-500 text-4xl mr-3"></i> | |
| <h1 class="text-3xl font-bold text-gray-800">Cycle<span class="text-pink-500">Sync</span></h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="newCycleBtn" class="bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded-lg shadow-md transition"> | |
| <i class="fas fa-plus mr-2"></i>New Cycle | |
| </button> | |
| <button id="settingsBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 p-2 rounded-full shadow-md transition"> | |
| <i class="fas fa-cog"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Dashboard --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <!-- User Info Panel --> | |
| <div class="bg-white rounded-xl shadow-md p-6 lg:col-span-1"> | |
| <div class="flex items-center mb-6"> | |
| <div class="w-16 h-16 rounded-full bg-pink-100 flex items-center justify-center mr-4"> | |
| <i class="fas fa-user text-pink-500 text-2xl"></i> | |
| </div> | |
| <div> | |
| <h2 class="text-xl font-semibold text-gray-800" id="userName">My Profile</h2> | |
| <p class="text-gray-500">Track your cycle with confidence</p> | |
| </div> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Cycle Length</label> | |
| <div class="flex items-center"> | |
| <input type="number" id="cycleLength" min="21" max="45" value="28" | |
| class="w-20 px-3 py-2 border border-gray-300 rounded-lg mr-2"> | |
| <span class="text-gray-600">days</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Period Length</label> | |
| <div class="flex items-center"> | |
| <input type="number" id="periodLength" min="2" max="10" value="5" | |
| class="w-20 px-3 py-2 border border-gray-300 rounded-lg mr-2"> | |
| <span class="text-gray-600">days</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Last Period Start</label> | |
| <input type="date" id="lastPeriodStart" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> | |
| </div> | |
| <button id="saveSettingsBtn" class="w-full bg-pink-500 hover:bg-pink-600 text-white py-2 rounded-lg shadow-md transition mt-4"> | |
| Save Settings | |
| </button> | |
| </div> | |
| <div class="mt-8 pt-6 border-t border-gray-200"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-3">Today's Status</h3> | |
| <div id="todayStatus" class="bg-blue-50 text-blue-800 p-4 rounded-lg"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-info-circle text-blue-500 mr-2"></i> | |
| <span>Update your settings to see your cycle status</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Calendar and Tracking --> | |
| <div class="bg-white rounded-xl shadow-md p-6 lg:col-span-2"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-semibold text-gray-800">Cycle Calendar</h2> | |
| <div class="flex items-center space-x-2"> | |
| <button id="prevMonthBtn" class="p-2 rounded-full hover:bg-gray-100"> | |
| <i class="fas fa-chevron-left text-gray-600"></i> | |
| </button> | |
| <span id="currentMonthYear" class="font-medium text-gray-700">June 2023</span> | |
| <button id="nextMonthBtn" class="p-2 rounded-full hover:bg-gray-100"> | |
| <i class="fas fa-chevron-right text-gray-600"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Calendar Grid --> | |
| <div class="mb-6"> | |
| <div class="grid grid-cols-7 gap-1 mb-2"> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Sun</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Mon</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Tue</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Wed</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Thu</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Fri</div> | |
| <div class="text-center text-sm font-medium text-gray-500 py-1">Sat</div> | |
| </div> | |
| <div id="calendarGrid" class="grid grid-cols-7 gap-1"> | |
| <!-- Calendar days will be inserted here by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Cycle Phases --> | |
| <div class="mb-6"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-3">Current Cycle Phases</h3> | |
| <div id="cyclePhases" class="flex overflow-x-auto custom-scrollbar pb-2"> | |
| <!-- Cycle phases will be inserted here by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Symptoms Tracker --> | |
| <div> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h3 class="text-lg font-medium text-gray-800">Today's Symptoms</h3> | |
| <button id="addSymptomBtn" class="text-pink-500 hover:text-pink-600 text-sm"> | |
| <i class="fas fa-plus mr-1"></i>Add | |
| </button> | |
| </div> | |
| <div id="symptomsList" class="flex flex-wrap gap-2"> | |
| <!-- Symptoms will be inserted here by JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Statistics Section --> | |
| <div class="bg-white rounded-xl shadow-md p-6 mt-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Cycle Statistics</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="bg-blue-50 p-4 rounded-lg"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-blue-600">Average Cycle</p> | |
| <p class="text-2xl font-bold text-blue-800" id="avgCycleLength">28 days</p> | |
| </div> | |
| <i class="fas fa-calendar-alt text-blue-400 text-2xl"></i> | |
| </div> | |
| </div> | |
| <div class="bg-purple-50 p-4 rounded-lg"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-purple-600">Average Period</p> | |
| <p class="text-2xl font-bold text-purple-800" id="avgPeriodLength">5 days</p> | |
| </div> | |
| <i class="fas fa-tint text-purple-400 text-2xl"></i> | |
| </div> | |
| </div> | |
| <div class="bg-green-50 p-4 rounded-lg"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-green-600">Next Period</p> | |
| <p class="text-2xl font-bold text-green-800" id="nextPeriodDate">--</p> | |
| </div> | |
| <i class="fas fa-clock text-green-400 text-2xl"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notes Section --> | |
| <div class="bg-white rounded-xl shadow-md p-6 mt-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-3">Cycle Notes</h2> | |
| <textarea id="cycleNotes" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-200 focus:border-pink-300" rows="4" placeholder="Record any notes about your cycle, symptoms, or moods..."></textarea> | |
| <button id="saveNotesBtn" class="mt-3 bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded-lg shadow-md transition"> | |
| Save Notes | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Symptom Modal --> | |
| <div id="symptomModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-xl p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Add Symptom</h3> | |
| <button id="closeSymptomModal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Symptom</label> | |
| <select id="symptomType" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> | |
| <option value="cramps">Cramps</option> | |
| <option value="headache">Headache</option> | |
| <option value="bloating">Bloating</option> | |
| <option value="tender_breasts">Tender Breasts</option> | |
| <option value="acne">Acne</option> | |
| <option value="fatigue">Fatigue</option> | |
| <option value="mood_swings">Mood Swings</option> | |
| <option value="nausea">Nausea</option> | |
| <option value="backache">Backache</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Intensity</label> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-gray-500 text-sm">Low</span> | |
| <input type="range" id="symptomIntensity" min="1" max="5" value="3" class="w-full"> | |
| <span class="text-gray-500 text-sm">High</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Notes</label> | |
| <textarea id="symptomNotes" class="w-full px-3 py-2 border border-gray-300 rounded-lg" rows="2" placeholder="Any additional details..."></textarea> | |
| </div> | |
| <button id="saveSymptomBtn" class="w-full bg-pink-500 hover:bg-pink-600 text-white py-2 rounded-lg shadow-md transition"> | |
| Add Symptom | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const elements = { | |
| calendarGrid: document.getElementById('calendarGrid'), | |
| currentMonthYear: document.getElementById('currentMonthYear'), | |
| prevMonthBtn: document.getElementById('prevMonthBtn'), | |
| nextMonthBtn: document.getElementById('nextMonthBtn'), | |
| cycleLength: document.getElementById('cycleLength'), | |
| periodLength: document.getElementById('periodLength'), | |
| lastPeriodStart: document.getElementById('lastPeriodStart'), | |
| saveSettingsBtn: document.getElementById('saveSettingsBtn'), | |
| todayStatus: document.getElementById('todayStatus'), | |
| cyclePhases: document.getElementById('cyclePhases'), | |
| symptomsList: document.getElementById('symptomsList'), | |
| addSymptomBtn: document.getElementById('addSymptomBtn'), | |
| symptomModal: document.getElementById('symptomModal'), | |
| closeSymptomModal: document.getElementById('closeSymptomModal'), | |
| saveSymptomBtn: document.getElementById('saveSymptomBtn'), | |
| symptomType: document.getElementById('symptomType'), | |
| symptomIntensity: document.getElementById('symptomIntensity'), | |
| symptomNotes: document.getElementById('symptomNotes'), | |
| avgCycleLength: document.getElementById('avgCycleLength'), | |
| avgPeriodLength: document.getElementById('avgPeriodLength'), | |
| nextPeriodDate: document.getElementById('nextPeriodDate'), | |
| cycleNotes: document.getElementById('cycleNotes'), | |
| saveNotesBtn: document.getElementById('saveNotesBtn'), | |
| newCycleBtn: document.getElementById('newCycleBtn') | |
| }; | |
| // State | |
| let state = { | |
| currentDate: new Date(), | |
| userSettings: { | |
| cycleLength: 28, | |
| periodLength: 5, | |
| lastPeriodStart: null, | |
| userName: 'My Profile' | |
| }, | |
| cycleData: [], | |
| symptoms: {}, | |
| notes: '' | |
| }; | |
| // Initialize | |
| function init() { | |
| loadData(); | |
| renderCalendar(); | |
| updateCyclePhases(); | |
| updateTodayStatus(); | |
| updateStatistics(); | |
| setupEventListeners(); | |
| } | |
| // Load data from localStorage | |
| function loadData() { | |
| const savedSettings = localStorage.getItem('cycleTrackerSettings'); | |
| const savedCycleData = localStorage.getItem('cycleTrackerData'); | |
| const savedSymptoms = localStorage.getItem('cycleTrackerSymptoms'); | |
| const savedNotes = localStorage.getItem('cycleTrackerNotes'); | |
| if (savedSettings) { | |
| state.userSettings = JSON.parse(savedSettings); | |
| elements.cycleLength.value = state.userSettings.cycleLength; | |
| elements.periodLength.value = state.userSettings.periodLength; | |
| if (state.userSettings.lastPeriodStart) { | |
| elements.lastPeriodStart.value = state.userSettings.lastPeriodStart; | |
| } | |
| } | |
| if (savedCycleData) { | |
| state.cycleData = JSON.parse(savedCycleData); | |
| } | |
| if (savedSymptoms) { | |
| state.symptoms = JSON.parse(savedSymptoms); | |
| renderSymptoms(); | |
| } | |
| if (savedNotes) { | |
| state.notes = savedNotes; | |
| elements.cycleNotes.value = state.notes; | |
| } | |
| } | |
| // Save data to localStorage | |
| function saveData() { | |
| localStorage.setItem('cycleTrackerSettings', JSON.stringify(state.userSettings)); | |
| localStorage.setItem('cycleTrackerData', JSON.stringify(state.cycleData)); | |
| localStorage.setItem('cycleTrackerSymptoms', JSON.stringify(state.symptoms)); | |
| localStorage.setItem('cycleTrackerNotes', state.notes); | |
| } | |
| // Render calendar | |
| function renderCalendar() { | |
| elements.calendarGrid.innerHTML = ''; | |
| const year = state.currentDate.getFullYear(); | |
| const month = state.currentDate.getMonth(); | |
| // Set month and year in header | |
| elements.currentMonthYear.textContent = new Intl.DateTimeFormat('en-US', { | |
| month: 'long', | |
| year: 'numeric' | |
| }).format(state.currentDate); | |
| // Get first day of month and total days in month | |
| const firstDay = new Date(year, month, 1).getDay(); | |
| const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
| // Get days from previous month to display | |
| const prevMonthDays = new Date(year, month, 0).getDate(); | |
| // Add days from previous month | |
| for (let i = 0; i < firstDay; i++) { | |
| const day = prevMonthDays - firstDay + i + 1; | |
| const date = new Date(year, month - 1, day); | |
| const dayElement = createDayElement(date, true); | |
| elements.calendarGrid.appendChild(dayElement); | |
| } | |
| // Add days from current month | |
| for (let i = 1; i <= daysInMonth; i++) { | |
| const date = new Date(year, month, i); | |
| const dayElement = createDayElement(date, false); | |
| elements.calendarGrid.appendChild(dayElement); | |
| } | |
| // Calculate how many days from next month to display (to fill the grid) | |
| const totalDaysDisplayed = firstDay + daysInMonth; | |
| const nextMonthDays = 7 - (totalDaysDisplayed % 7); | |
| if (nextMonthDays < 7) { | |
| for (let i = 1; i <= nextMonthDays; i++) { | |
| const date = new Date(year, month + 1, i); | |
| const dayElement = createDayElement(date, true); | |
| elements.calendarGrid.appendChild(dayElement); | |
| } | |
| } | |
| } | |
| // Create day element for calendar | |
| function createDayElement(date, isOtherMonth) { | |
| const dayElement = document.createElement('div'); | |
| dayElement.className = 'h-24 p-1 border border-gray-100 rounded-lg overflow-hidden'; | |
| const dayNumber = document.createElement('div'); | |
| dayNumber.className = 'text-right text-sm font-medium mb-1'; | |
| dayNumber.textContent = date.getDate(); | |
| const dayContent = document.createElement('div'); | |
| dayContent.className = 'text-xs h-16 overflow-y-auto custom-scrollbar'; | |
| dayElement.appendChild(dayNumber); | |
| dayElement.appendChild(dayContent); | |
| if (isOtherMonth) { | |
| dayElement.classList.add('bg-gray-50', 'text-gray-400'); | |
| } else { | |
| // Check if this day is in the current cycle | |
| if (state.userSettings.lastPeriodStart) { | |
| const lastPeriodStart = new Date(state.userSettings.lastPeriodStart); | |
| const cycleEnd = new Date(lastPeriodStart); | |
| cycleEnd.setDate(cycleEnd.getDate() + state.userSettings.cycleLength); | |
| if (date >= lastPeriodStart && date < cycleEnd) { | |
| const dayInCycle = Math.floor((date - lastPeriodStart) / (1000 * 60 * 60 * 24)) + 1; | |
| // Mark period days | |
| if (dayInCycle <= state.userSettings.periodLength) { | |
| dayElement.classList.add('period-day'); | |
| const periodBadge = document.createElement('div'); | |
| periodBadge.className = 'bg-pink-500 text-white text-xs px-1 rounded inline-block mb-1'; | |
| periodBadge.textContent = 'Period'; | |
| dayContent.appendChild(periodBadge); | |
| } | |
| // Mark fertile window (5 days before ovulation and ovulation day) | |
| const ovulationDay = state.userSettings.cycleLength - 14; | |
| const fertileStart = ovulationDay - 5; | |
| const fertileEnd = ovulationDay; | |
| if (dayInCycle >= fertileStart && dayInCycle <= fertileEnd) { | |
| dayElement.classList.add('fertile-window'); | |
| const fertileBadge = document.createElement('div'); | |
| fertileBadge.className = 'bg-green-500 text-white text-xs px-1 rounded inline-block mb-1'; | |
| fertileBadge.textContent = dayInCycle === ovulationDay ? 'Ovulation' : 'Fertile'; | |
| dayContent.appendChild(fertileBadge); | |
| } | |
| } | |
| } | |
| // Mark today | |
| const today = new Date(); | |
| if (date.getDate() === today.getDate() && | |
| date.getMonth() === today.getMonth() && | |
| date.getFullYear() === today.getFullYear()) { | |
| dayElement.classList.add('ring-2', 'ring-pink-400'); | |
| } | |
| // Add symptoms for this day | |
| const dateString = formatDate(date); | |
| if (state.symptoms[dateString]) { | |
| state.symptoms[dateString].forEach(symptom => { | |
| const symptomElement = document.createElement('div'); | |
| symptomElement.className = 'flex items-center mb-1'; | |
| const icon = document.createElement('i'); | |
| icon.className = getSymptomIcon(symptom.type) + ' text-xs mr-1'; | |
| const intensity = document.createElement('div'); | |
| intensity.className = 'w-2 h-2 rounded-full mr-1'; | |
| intensity.style.backgroundColor = getIntensityColor(symptom.intensity); | |
| symptomElement.appendChild(icon); | |
| symptomElement.appendChild(intensity); | |
| symptomElement.appendChild(document.createTextNode(symptom.type)); | |
| dayContent.appendChild(symptomElement); | |
| }); | |
| } | |
| } | |
| return dayElement; | |
| } | |
| // Get icon for symptom type | |
| function getSymptomIcon(type) { | |
| const icons = { | |
| cramps: 'fas fa-bolt', | |
| headache: 'fas fa-head-side-virus', | |
| bloating: 'fas fa-weight', | |
| tender_breasts: 'fas fa-female', | |
| acne: 'fas fa-allergies', | |
| fatigue: 'fas fa-bed', | |
| mood_swings: 'fas fa-smile', | |
| nausea: 'fas fa-stomach', | |
| backache: 'fas fa-bone' | |
| }; | |
| return icons[type] || 'fas fa-dot-circle'; | |
| } | |
| // Get color for symptom intensity | |
| function getIntensityColor(intensity) { | |
| const colors = ['#93c5fd', '#60a5fa', '#3b82f6', '#2563eb', '#1d4ed8']; | |
| return colors[intensity - 1] || '#93c5fd'; | |
| } | |
| // Update cycle phases visualization | |
| function updateCyclePhases() { | |
| elements.cyclePhases.innerHTML = ''; | |
| if (!state.userSettings.lastPeriodStart) return; | |
| const cycleDays = state.userSettings.cycleLength; | |
| const periodDays = state.userSettings.periodLength; | |
| const ovulationDay = cycleDays - 14; | |
| const fertileStart = ovulationDay - 5; | |
| const fertileEnd = ovulationDay; | |
| const phases = [ | |
| { name: 'Period', start: 1, end: periodDays, color: 'bg-pink-500' }, | |
| { name: 'Follicular', start: periodDays + 1, end: fertileStart - 1, color: 'bg-blue-400' }, | |
| { name: 'Fertile', start: fertileStart, end: fertileEnd, color: 'bg-green-500' }, | |
| { name: 'Luteal', start: ovulationDay + 1, end: cycleDays, color: 'bg-purple-500' } | |
| ]; | |
| phases.forEach(phase => { | |
| const phaseElement = document.createElement('div'); | |
| phaseElement.className = 'flex-shrink-0 mr-4'; | |
| const phaseTitle = document.createElement('h4'); | |
| phaseTitle.className = 'text-sm font-medium text-gray-700 mb-1'; | |
| phaseTitle.textContent = phase.name; | |
| const phaseBar = document.createElement('div'); | |
| phaseBar.className = 'h-4 rounded-full overflow-hidden flex'; | |
| const daysInPhase = phase.end - phase.start + 1; | |
| const phaseWidth = (daysInPhase / cycleDays) * 100; | |
| const phaseColor = document.createElement('div'); | |
| phaseColor.className = phase.color; | |
| phaseColor.style.width = `${phaseWidth}%`; | |
| const emptySpace = document.createElement('div'); | |
| emptySpace.className = 'bg-gray-200'; | |
| emptySpace.style.width = `${100 - phaseWidth}%`; | |
| phaseBar.appendChild(phaseColor); | |
| phaseBar.appendChild(emptySpace); | |
| phaseElement.appendChild(phaseTitle); | |
| phaseElement.appendChild(phaseBar); | |
| elements.cyclePhases.appendChild(phaseElement); | |
| }); | |
| } | |
| // Update today's status | |
| function updateTodayStatus() { | |
| if (!state.userSettings.lastPeriodStart) return; | |
| const today = new Date(); | |
| const lastPeriodStart = new Date(state.userSettings.lastPeriodStart); | |
| const cycleEnd = new Date(lastPeriodStart); | |
| cycleEnd.setDate(cycleEnd.getDate() + state.userSettings.cycleLength); | |
| if (today < lastPeriodStart || today >= cycleEnd) { | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-calendar-check text-blue-500 mr-2"></i> | |
| <span>You're not currently in a tracked cycle. Start a new cycle when your period begins.</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-blue-50 text-blue-800 p-4 rounded-lg'; | |
| return; | |
| } | |
| const dayInCycle = Math.floor((today - lastPeriodStart) / (1000 * 60 * 60 * 24)) + 1; | |
| const ovulationDay = state.userSettings.cycleLength - 14; | |
| const fertileStart = ovulationDay - 5; | |
| const fertileEnd = ovulationDay; | |
| if (dayInCycle <= state.userSettings.periodLength) { | |
| // Period | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-tint text-pink-500 mr-2"></i> | |
| <span><strong>Day ${dayInCycle} of Period:</strong> You're currently on your period.</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-pink-50 text-pink-800 p-4 rounded-lg'; | |
| } else if (dayInCycle >= fertileStart && dayInCycle <= fertileEnd) { | |
| // Fertile window | |
| if (dayInCycle === ovulationDay) { | |
| // Ovulation day | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-egg text-yellow-500 mr-2"></i> | |
| <span><strong>Ovulation Day:</strong> You're most fertile today!</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-yellow-50 text-yellow-800 p-4 rounded-lg'; | |
| } else { | |
| // Fertile days | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-seedling text-green-500 mr-2"></i> | |
| <span><strong>Fertile Window (Day ${dayInCycle}):</strong> High chance of conception.</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-green-50 text-green-800 p-4 rounded-lg'; | |
| } | |
| } else if (dayInCycle > ovulationDay) { | |
| // Luteal phase | |
| const daysUntilPeriod = state.userSettings.cycleLength - dayInCycle; | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-clock text-purple-500 mr-2"></i> | |
| <span><strong>Luteal Phase (Day ${dayInCycle}):</strong> ${daysUntilPeriod} days until your next period.</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-purple-50 text-purple-800 p-4 rounded-lg'; | |
| } else { | |
| // Follicular phase | |
| elements.todayStatus.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-leaf text-blue-500 mr-2"></i> | |
| <span><strong>Follicular Phase (Day ${dayInCycle}):</strong> Your body is preparing for ovulation.</span> | |
| </div> | |
| `; | |
| elements.todayStatus.className = 'bg-blue-50 text-blue-800 p-4 rounded-lg'; | |
| } | |
| } | |
| // Update statistics | |
| function updateStatistics() { | |
| if (state.cycleData.length > 0) { | |
| // Calculate average cycle length | |
| const totalCycles = state.cycleData.length; | |
| const totalDays = state.cycleData.reduce((sum, cycle) => sum + cycle.length, 0); | |
| const avgCycle = Math.round(totalDays / totalCycles); | |
| elements.avgCycleLength.textContent = `${avgCycle} days`; | |
| // Calculate average period length | |
| const totalPeriodDays = state.cycleData.reduce((sum, cycle) => sum + (cycle.periodLength || state.userSettings.periodLength), 0); | |
| const avgPeriod = Math.round(totalPeriodDays / totalCycles); | |
| elements.avgPeriodLength.textContent = `${avgPeriod} days`; | |
| } else { | |
| elements.avgCycleLength.textContent = `${state.userSettings.cycleLength} days`; | |
| elements.avgPeriodLength.textContent = `${state.userSettings.periodLength} days`; | |
| } | |
| // Calculate next period date | |
| if (state.userSettings.lastPeriodStart) { | |
| const lastPeriodStart = new Date(state.userSettings.lastPeriodStart); | |
| const nextPeriodStart = new Date(lastPeriodStart); | |
| nextPeriodStart.setDate(nextPeriodStart.getDate() + state.userSettings.cycleLength); | |
| const today = new Date(); | |
| if (nextPeriodStart > today) { | |
| const options = { weekday: 'long', month: 'long', day: 'numeric' }; | |
| elements.nextPeriodDate.textContent = nextPeriodStart.toLocaleDateString('en-US', options); | |
| } else { | |
| elements.nextPeriodDate.textContent = '--'; | |
| } | |
| } else { | |
| elements.nextPeriodDate.textContent = '--'; | |
| } | |
| } | |
| // Render symptoms | |
| function renderSymptoms() { | |
| elements.symptomsList.innerHTML = ''; | |
| const today = formatDate(new Date()); | |
| if (state.symptoms[today]) { | |
| state.symptoms[today].forEach((symptom, index) => { | |
| const symptomElement = document.createElement('div'); | |
| symptomElement.className = 'flex items-center bg-gray-100 px-3 py-2 rounded-full symptom-icon'; | |
| const icon = document.createElement('i'); | |
| icon.className = getSymptomIcon(symptom.type) + ' text-pink-500 mr-2'; | |
| const intensity = document.createElement('div'); | |
| intensity.className = 'w-2 h-2 rounded-full mr-2'; | |
| intensity.style.backgroundColor = getIntensityColor(symptom.intensity); | |
| const text = document.createElement('span'); | |
| text.className = 'text-sm text-gray-700 mr-2'; | |
| text.textContent = symptom.type; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'text-gray-400 hover:text-red-500'; | |
| deleteBtn.innerHTML = '<i class="fas fa-times"></i>'; | |
| deleteBtn.onclick = () => removeSymptom(today, index); | |
| symptomElement.appendChild(icon); | |
| symptomElement.appendChild(intensity); | |
| symptomElement.appendChild(text); | |
| symptomElement.appendChild(deleteBtn); | |
| elements.symptomsList.appendChild(symptomElement); | |
| }); | |
| } | |
| } | |
| // Remove symptom | |
| function removeSymptom(dateString, index) { | |
| if (state.symptoms[dateString]) { | |
| state.symptoms[dateString].splice(index, 1); | |
| if (state.symptoms[dateString].length === 0) { | |
| delete state.symptoms[dateString]; | |
| } | |
| saveData(); | |
| renderSymptoms(); | |
| renderCalendar(); | |
| } | |
| } | |
| // Format date as YYYY-MM-DD | |
| function formatDate(date) { | |
| const year = date.getFullYear(); | |
| const month = String(date.getMonth() + 1).padStart(2, '0'); | |
| const day = String(date.getDate()).padStart(2, '0'); | |
| return `${year}-${month}-${day}`; | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Navigation buttons | |
| elements.prevMonthBtn.addEventListener('click', () => { | |
| state.currentDate.setMonth(state.currentDate.getMonth() - 1); | |
| renderCalendar(); | |
| }); | |
| elements.nextMonthBtn.addEventListener('click', () => { | |
| state.currentDate.setMonth(state.currentDate.getMonth() + 1); | |
| renderCalendar(); | |
| }); | |
| // Save settings | |
| elements.saveSettingsBtn.addEventListener('click', () => { | |
| state.userSettings.cycleLength = parseInt(elements.cycleLength.value); | |
| state.userSettings.periodLength = parseInt(elements.periodLength.value); | |
| state.userSettings.lastPeriodStart = elements.lastPeriodStart.value; | |
| // Record this cycle if it's a new period start | |
| if (state.userSettings.lastPeriodStart) { | |
| const lastPeriodDate = new Date(state.userSettings.lastPeriodStart); | |
| const existingCycle = state.cycleData.find(cycle => | |
| new Date(cycle.startDate).getTime() === lastPeriodDate.getTime() | |
| ); | |
| if (!existingCycle) { | |
| state.cycleData.push({ | |
| startDate: state.userSettings.lastPeriodStart, | |
| length: state.userSettings.cycleLength, | |
| periodLength: state.userSettings.periodLength | |
| }); | |
| // Keep only the last 12 cycles | |
| if (state.cycleData.length > 12) { | |
| state.cycleData.shift(); | |
| } | |
| } | |
| } | |
| saveData(); | |
| renderCalendar(); | |
| updateCyclePhases(); | |
| updateTodayStatus(); | |
| updateStatistics(); | |
| }); | |
| // Symptoms | |
| elements.addSymptomBtn.addEventListener('click', () => { | |
| elements.symptomModal.classList.remove('hidden'); | |
| }); | |
| elements.closeSymptomModal.addEventListener('click', () => { | |
| elements.symptomModal.classList.add('hidden'); | |
| }); | |
| elements.saveSymptomBtn.addEventListener('click', () => { | |
| const today = formatDate(new Date()); | |
| const symptom = { | |
| type: elements.symptomType.value, | |
| intensity: parseInt(elements.symptomIntensity.value), | |
| notes: elements.symptomNotes.value, | |
| date: today | |
| }; | |
| if (!state.symptoms[today]) { | |
| state.symptoms[today] = []; | |
| } | |
| state.symptoms[today].push(symptom); | |
| saveData(); | |
| renderSymptoms(); | |
| renderCalendar(); | |
| elements.symptomModal.classList.add('hidden'); | |
| // Reset form | |
| elements.symptomType.value = 'cramps'; | |
| elements.symptomIntensity.value = 3; | |
| elements.symptomNotes.value = ''; | |
| }); | |
| // Notes | |
| elements.saveNotesBtn.addEventListener('click', () => { | |
| state.notes = elements.cycleNotes.value; | |
| saveData(); | |
| // Show confirmation | |
| const originalText = elements.saveNotesBtn.textContent; | |
| elements.saveNotesBtn.textContent = 'Saved!'; | |
| elements.saveNotesBtn.classList.remove('bg-pink-500', 'hover:bg-pink-600'); | |
| elements.saveNotesBtn.classList.add('bg-green-500', 'hover:bg-green-600'); | |
| setTimeout(() => { | |
| elements.saveNotesBtn.textContent = originalText; | |
| elements.saveNotesBtn.classList.remove('bg-green-500', 'hover:bg-green-600'); | |
| elements.saveNotesBtn.classList.add('bg-pink-500', 'hover:bg-pink-600'); | |
| }, 2000); | |
| }); | |
| // New cycle button | |
| elements.newCycleBtn.addEventListener('click', () => { | |
| const today = formatDate(new Date()); | |
| elements.lastPeriodStart.value = today; | |
| state.userSettings.lastPeriodStart = today; | |
| // Record this cycle | |
| state.cycleData.push({ | |
| startDate: today, | |
| length: state.userSettings.cycleLength, | |
| periodLength: state.userSettings.periodLength | |
| }); | |
| // Keep only the last 12 cycles | |
| if (state.cycleData.length > 12) { | |
| state.cycleData.shift(); | |
| } | |
| saveData(); | |
| renderCalendar(); | |
| updateCyclePhases(); | |
| updateTodayStatus(); | |
| updateStatistics(); | |
| }); | |
| // Set last period start to today by default if empty | |
| if (!elements.lastPeriodStart.value) { | |
| elements.lastPeriodStart.value = formatDate(new Date()); | |
| } | |
| } | |
| // Initialize the app | |
| init(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Axelle124125/cycletracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |