├── sw.js ├── circle.svg ├── tire.svg ├── style.css ├── manifest.json ├── index.html ├── app.js └── wheel.svg /sw.js: -------------------------------------------------------------------------------- 1 | const VERSION = "v1"; 2 | 3 | -------------------------------------------------------------------------------- /circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tire.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 1vh 1vw; 3 | background-color: #efe; 4 | } 5 | ul, 6 | fieldset, 7 | legend { 8 | border: 1px solid; 9 | background-color: #fff; 10 | } 11 | ul { 12 | padding: 0; 13 | font-family: monospace; 14 | } 15 | li, 16 | legend { 17 | list-style-type: none; 18 | padding: 0.2em 0.5em; 19 | background-color: #cfc; 20 | } 21 | li:nth-of-type(even) { 22 | background-color: inherit; 23 | } 24 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CycleTracker: Period Tracking app", 3 | "short_name": "CT", 4 | "description": "Securely and confidentially track your menstrual cycle. Enter the start and end dates of your periods, saving your private data to your browser on your device, without sharing it with the rest of the world.", 5 | "start_url": "/", 6 | "theme_color": "#eeffee", 7 | "background_color": "#eeffee", 8 | "display": "standalone", 9 | "prefer_related_applications": false, 10 | "icons": [ 11 | { 12 | "src": "circle.ico", 13 | "sizes": "48x48" 14 | }, 15 | { 16 | "src": "circle.svg", 17 | "sizes": "72x72 96x96", 18 | "purpose": "maskable" 19 | }, 20 | { 21 | "src": "tire.svg", 22 | "sizes": "128x128 256x256" 23 | }, 24 | { 25 | "src": "wheel.svg", 26 | "sizes": "512x512" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cycle Tracker 7 | 8 | 9 | 10 | 11 | 12 |

Period tracker

13 |
14 |
15 | Enter your period start and end date 16 |

17 | 18 | 19 |

20 |

21 | 22 | 23 |

24 |
25 |

26 | 27 |

28 |
29 |
30 | 31 | 32 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const newPeriodFormEl = document.getElementsByTagName("form")[0]; 2 | const startDateInputEl = document.getElementById("start-date"); 3 | const endDateInputEl = document.getElementById("end-date"); 4 | const pastPeriodContainer = document.getElementById("past-periods"); 5 | 6 | // Add the storage key as an app-wide constant 7 | const STORAGE_KEY = "period-tracker"; 8 | 9 | // Listen to form submissions. 10 | newPeriodFormEl.addEventListener("submit", (event) => { 11 | event.preventDefault(); 12 | const startDate = startDateInputEl.value; 13 | const endDate = endDateInputEl.value; 14 | if (checkDatesInvalid(startDate, endDate)) { 15 | return; 16 | } 17 | storeNewPeriod(startDate, endDate); 18 | renderPastPeriods(); 19 | newPeriodFormEl.reset(); 20 | }); 21 | 22 | function checkDatesInvalid(startDate, endDate) { 23 | if (!startDate || !endDate || startDate > endDate) { 24 | newPeriodFormEl.reset(); 25 | return true; 26 | } 27 | return false; 28 | } 29 | 30 | function storeNewPeriod(startDate, endDate) { 31 | const periods = getAllStoredPeriods(); 32 | periods.push({ startDate, endDate }); 33 | periods.sort((a, b) => { 34 | return new Date(b.startDate) - new Date(a.startDate); 35 | }); 36 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify(periods)); 37 | } 38 | 39 | function getAllStoredPeriods() { 40 | const data = window.localStorage.getItem(STORAGE_KEY); 41 | const periods = data ? JSON.parse(data) : []; 42 | console.dir(periods); 43 | console.log(periods); 44 | return periods; 45 | } 46 | 47 | function renderPastPeriods() { 48 | const pastPeriodHeader = document.createElement("h2"); 49 | const pastPeriodList = document.createElement("ul"); 50 | const periods = getAllStoredPeriods(); 51 | if (periods.length === 0) { 52 | return; 53 | } 54 | pastPeriodContainer.textContent = ""; 55 | pastPeriodHeader.textContent = "Past periods"; 56 | periods.forEach((period) => { 57 | const periodEl = document.createElement("li"); 58 | periodEl.textContent = `From ${formatDate( 59 | period.startDate, 60 | )} to ${formatDate(period.endDate)}`; 61 | pastPeriodList.appendChild(periodEl); 62 | }); 63 | 64 | pastPeriodContainer.appendChild(pastPeriodHeader); 65 | pastPeriodContainer.appendChild(pastPeriodList); 66 | } 67 | 68 | function formatDate(dateString) { 69 | const date = new Date(dateString); 70 | return date.toLocaleDateString("en-US", { timeZone: "UTC" }); 71 | } 72 | 73 | renderPastPeriods(); 74 | -------------------------------------------------------------------------------- /wheel.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | --------------------------------------------------------------------------------