view the Web --> https://herhelth.netlify.app/
A Women's Day special single-page wellness companion built with pure HTML5, CSS3, and vanilla JavaScript. No frameworks. No build tools. No backend. Just open
index.htmland go.
- Overview
- Features
- Getting Started
- Project Structure
- Architecture
- Modules
- Data Persistence
- Design System
- Error Handling
- Testing
- Browser Compatibility
HerHealth is a unified daily wellness hub themed around Women's Day — "Healthy Women, Stronger World". It gives women a single place to track water intake, sleep, mood, menstrual cycles, reminders, nutrition, and exercise, all without requiring an account, a server, or an internet connection after the first load.
All user data is stored locally in the browser via localStorage. The app works offline and can be opened directly from the file system via a file:// URL.
| Feature | Description |
|---|---|
| 🌸 Women's Day Banner | Animated hero banner with rotating inspirational quotes |
| 💧 Water Tracker | Log daily glasses consumed toward an 8-glass target |
| 😴 Sleep Logger | Record nightly sleep hours (0–24) |
| 📅 Period Tracker | Predict next period from last date + cycle length |
| ⏰ Reminders | Browser notifications for Water, Exercise, Medicine, Sleep |
| 🥗 Nutrition Tips | Daily meal suggestions that rotate by day of the week |
| 🏃 Exercise & Yoga | Beginner workout cards and yoga poses for stress relief |
| 😊 Mood Tracker | Log Happy / Sad / Stressed with contextual support messages |
| ✨ Daily Motivation | Random motivational quotes with a "New Quote" button |
| 🎯 Health Score | Live 0–100 score based on daily goal completions |
| 🌙 Dark Mode | Full light/dark theme toggle, persisted across sessions |
No installation required.
# Clone or download the repository
git clone <repo-url>
cd herhealth-wellness-hub
# Open directly in your browser
open index.html # macOS
start index.html # Windows
xdg-open index.html # LinuxThat's it. The app runs entirely in the browser with no server needed.
The test suite uses Vitest with jsdom.
# Install dev dependencies (one-time)
npm install
# Run all tests once
npm test
# or
npx vitest --runherhealth-wellness-hub/
├── index.html # The entire application (HTML + CSS + JS)
├── package.json # Dev dependencies (vitest, jsdom)
├── vitest.config.js # Vitest configuration
└── tests/
├── helpers/
│ └── loadPage.js # jsdom helper — loads index.html into a test environment
└── unit/
├── banner.test.js
├── dashboard.test.js
├── exercise.test.js
├── health-score.test.js
├── mood.test.js
├── motivation.test.js
├── nutrition.test.js
├── period-tracker.test.js
├── reminder.test.js
└── theme.test.js
Everything lives in index.html. The CSS and JavaScript are embedded directly — no external files to serve.
The app follows a modular IIFE (Immediately Invoked Function Expression) architecture. Each feature is a self-contained module that:
- Owns its own DOM section
- Manages its own
localStoragekeys - Registers its own event listeners
- Exposes a minimal public API
A thin App controller bootstraps all modules in order on DOMContentLoaded.
App.init()
├── ThemeModule.init()
├── BannerModule.init()
├── HealthScoreModule.init()
├── DashboardModule.init()
├── PeriodTrackerModule.init()
├── ReminderModule.init()
├── NutritionModule.init()
├── ExerciseModule.init()
├── MoodModule.init()
└── MotivationModule.init()
Data flow:
- On page load,
App.init()calls each module'sinit(). - Each module reads its
localStoragekeys and hydrates the DOM. - User interactions trigger module handlers → update DOM +
localStorage. - Modules that affect the health score call
HealthScoreModule.recalculate(). ReminderModuleruns asetIntervalat 60-second resolution to fire scheduled notifications.
Modules that feed into HealthScoreModule:
DashboardModule ──(water, sleep)──┐
MoodModule ──(mood)──────────┼──► HealthScoreModule.recalculate()
ReminderModule ──(reminder)──────┘
Manages the light/dark color theme.
Public API:
ThemeModule.init() // Reads hh_theme from localStorage, applies theme before first paint
ThemeModule.toggle() // Switches between 'light' and 'dark', persists preference
ThemeModule.getTheme() // Returns 'light' | 'dark'localStorage key: hh_theme
Example — reading the current theme:
const theme = ThemeModule.getTheme(); // 'light' or 'dark'Behavior:
- Defaults to
'light'when no preference is stored - Applies the
darkCSS class to<body>for dark mode - Theme switch completes within 300ms via CSS transition
- The toggle button (🌙 / ☀️) lives in the sticky nav bar
Renders the Women's Day hero banner with a random inspirational quote.
Public API:
BannerModule.init() // Picks a random quote and renders it into #banner-quoteNo persistent state.
Example quote rendered:
"She believed she could, so she did. — R.S. Grey"
The banner uses a CSS bannerFadeIn animation on load and displays the tagline "Healthy Women, Stronger World".
The main daily metrics hub — tracks water intake and sleep hours, and provides shortcuts to mood and health score.
Public API:
DashboardModule.init() // Restores state, checks date rollover
DashboardModule.incrementWater() // +1 glass (no upper limit), persists, recalculates score
DashboardModule.decrementWater() // -1 glass (floor: 0), persists, recalculates score
DashboardModule.saveSleep(hours) // Validates 0–24, persists, recalculates score
DashboardModule.resetIfNewDay() // Resets daily metrics if hh_last_date ≠ todaylocalStorage keys: hh_water, hh_sleep, hh_last_date
Example — saving sleep hours:
DashboardModule.saveSleep(7.5); // Saves 7.5 hours, updates health score
DashboardModule.saveSleep(25); // Invalid — shows error, does not saveDate rollover behavior:
When the page loads on a new calendar day, resetIfNewDay() automatically clears hh_water and hh_sleep back to their defaults so each day starts fresh.
Predicts the next menstrual period based on the last period date and cycle length.
Public API:
PeriodTrackerModule.init() // Restores saved data, re-renders last prediction
PeriodTrackerModule.calculate(lastDate, cycleLength) // Validates inputs, computes next date, persists
PeriodTrackerModule.renderPrediction(date) // Formats and displays the predicted date
PeriodTrackerModule.showError(message) // Shows an inline validation errorlocalStorage key: hh_period → { lastDate: "YYYY-MM-DD", cycleLength: number }
Validation rules:
| Input | Valid Range | Error shown when |
|---|---|---|
| Last period date | Any valid date | Empty or non-date string |
| Cycle length | 20–45 days | Outside this range |
Example:
Last period date: 2025-07-01
Cycle length: 28 days
→ Predicted next: Wednesday, 30 July 2025
Schedules browser notifications (or in-app fallback alerts) for four wellness categories.
Public API:
ReminderModule.init() // Restores configs, starts 60s polling interval
ReminderModule.enableReminder(category, time, sound) // Enables a reminder, requests notification permission
ReminderModule.disableReminder(category) // Disables a reminder
ReminderModule.poll() // Checks current time against all enabled reminders
ReminderModule.requestPermission() // Wraps Notification.requestPermission()
ReminderModule.dispatch(category) // Fires notification or in-app bannerlocalStorage key: hh_reminders → array of ReminderConfig
Supported categories:
| Category | Emoji | Default time |
|---|---|---|
| Water | 💧 | 08:00 |
| Exercise | 🏃 | 08:00 |
| Medicine | 💊 | 08:00 |
| Sleep | 😴 | 08:00 |
Example config stored in localStorage:
[
{ "category": "water", "time": "09:00", "enabled": true, "sound": true },
{ "category": "exercise", "time": "07:30", "enabled": true, "sound": false },
{ "category": "medicine", "time": "08:00", "enabled": false, "sound": false },
{ "category": "sleep", "time": "22:00", "enabled": true, "sound": false }
]Notification fallback: If the browser denies notification permission, an in-app banner appears at the scheduled time and auto-dismisses after 5 seconds.
Displays daily healthy meal suggestions that rotate by day of the week.
Public API:
NutritionModule.init() // Computes today's day index, renders suggestions
NutritionModule.getDayIndex() // Returns 0 (Sunday) – 6 (Saturday)No persistent state — content is deterministic by date.
Each meal slot (Breakfast, Lunch, Dinner) has 7 suggestions — one per day of the week.
Example (Monday = index 1):
| Meal | Suggestion |
|---|---|
| 🌅 Breakfast | Greek Yogurt & Honey 🍯 |
| ☀️ Lunch | Lentil Soup 🍲 |
| 🌙 Dinner | Stir-Fried Tofu & Greens 🥦 |
Renders beginner home workout cards and yoga poses for stress relief.
Public API:
ExerciseModule.init() // Renders all exercise and yoga cards from static dataNo persistent state.
Content:
- 5 exercise cards — Jumping Jacks, Bodyweight Squats, Push-Ups, Glute Bridges, High Knees
- 4 yoga cards — Child's Pose, Cat-Cow Stretch, Legs-Up-the-Wall, Seated Forward Fold
Each card includes a title, instruction text, and an emoji visual. The layout uses a responsive CSS grid that collapses to a single column on screens below 768px.
Lets users log their current mood and receive a contextual supportive message.
Public API:
MoodModule.init() // Restores last mood from localStorage, highlights selection
MoodModule.selectMood(mood) // 'happy' | 'sad' | 'stressed' — persists, shows message, recalculates score
MoodModule.renderMessage(mood) // Displays the contextual message for the given moodlocalStorage key: hh_mood → { mood: string, timestamp: string }
Mood messages:
| Mood | Message |
|---|---|
| 😊 Happy | "That's wonderful! Keep spreading your joy today. 🌟" |
| 😢 Sad | "It's okay to feel sad. Be gentle with yourself today. 💙" |
| 😰 Stressed | "Take a deep breath. You've got this! Try a quick yoga pose. 🧘" |
Displays a random motivational quote from a curated list of 12 women's wellness quotes.
Public API:
MotivationModule.init() // Picks a random quote and renders it
MotivationModule.newQuote() // Picks a DIFFERENT random quote and updates the display
MotivationModule.getQuotes() // Returns the full quotes array (used in tests)No persistent state.
Example quote:
"Self-care is not selfish. You cannot pour from an empty cup."
newQuote() guarantees the new quote is always different from the currently displayed one by filtering it out before selecting.
Computes and displays a live 0–100 wellness score based on four daily goal completions.
Public API:
HealthScoreModule.init() // Reads all metric states, renders initial score and progress bar
HealthScoreModule.recalculate() // Recomputes score from current localStorage state, animates bar
HealthScoreModule.getScore() // Returns the current score (0–100)localStorage key: hh_score
Score formula:
score = (completedGoals / 4) × 100
Each goal contributes 25 points:
| Goal | Condition | Points |
|---|---|---|
| 💧 Water | 8 or more glasses logged today | +25 |
| 😴 Sleep | Any sleep value saved today | +25 |
| 😊 Mood | A mood selected today | +25 |
| ⏰ Reminder | At least one reminder enabled | +25 |
Example scores:
0 goals completed → 0 / 100
2 goals completed → 50 / 100
4 goals completed → 100 / 100
The progress bar animates smoothly whenever any goal state changes.
All data is stored in localStorage under hh_* namespaced keys. No data ever leaves the browser.
| Key | Type | Description |
|---|---|---|
hh_theme |
'light' | 'dark' |
Current color theme |
hh_water |
number |
Glasses of water logged today |
hh_sleep |
number | null |
Sleep hours logged today |
hh_last_date |
string (ISO date) |
Date of last dashboard activity |
hh_period |
JSON object |
Last period date + cycle length |
hh_reminders |
JSON array |
All reminder configurations |
hh_mood |
JSON object |
Last selected mood + timestamp |
hh_score |
number |
Cached health score |
Corruption recovery: If any stored value fails JSON.parse(), the module silently resets that key to its default and continues. No data from other keys is affected.
Private browsing / storage blocked: If localStorage is unavailable, all modules operate in-memory for the session and a one-time warning banner is shown to the user.
The app uses a CSS custom property–based design system defined in :root.
/* Light mode */
--color-primary: #e91e8c; /* Hot pink — primary actions */
--color-secondary: #9c27b0; /* Purple — accents, secondary actions */
--color-accent: #ff4081; /* Bright pink — hover states */
--color-bg: #fff0f6; /* Soft pink background */
--color-surface: #ffffff; /* Card / panel background */
--color-text: #2d1b2e; /* Main text */
--color-text-muted: #7b5e7b; /* Secondary text *//* Dark mode (applied via body.dark) */
--color-bg: #1a0a1e;
--color-surface: #2d1b2e;
--color-text: #f8e8ff;
--color-text-muted: #ce93d8;| Breakpoint | Layout |
|---|---|
| < 768px | Single column (mobile-first default) |
| ≥ 768px | 2-column card grid |
| ≥ 1024px | 3-column card grid |
| ≥ 1200px | Wider section padding |
All interactive elements use CSS transitions for smooth feedback:
--transition-fast: 150ms ease; /* Button hover, input focus */
--transition-base: 250ms ease; /* Card hover */
--transition-slow: 300ms ease; /* Theme switch, background changes */| Scenario | Behavior |
|---|---|
| Period cycle length outside 20–45 | Inline error shown, prediction blocked |
| Period date empty or invalid | Inline error shown, prediction blocked |
| Sleep value outside 0–24 | Inline error shown, save blocked |
| Water count goes below 0 | Clamped to 0 silently |
| Notification permission denied | In-app banner fallback (auto-dismisses in 5s) |
localStorage unavailable |
In-memory mode + one-time warning banner |
Corrupted localStorage value |
Reset to default, clean value written back |
| New calendar day detected | Daily metrics (water, sleep) reset automatically |
The test suite uses Vitest as the test runner and jsdom to simulate the browser environment. All modules are IIFEs embedded in index.html, so tests load the full HTML file into jsdom and interact with the live DOM.
npm test # Run all tests once (--run flag)
npx vitest --run # Equivalent
npx vitest # Watch mode (for development)tests/helpers/loadPage.js provides a loadPage(localStorageData?) utility that:
- Reads
index.htmlfrom disk - Creates a fresh jsdom instance with
runScripts: 'dangerously' - Pre-seeds
localStoragewith any provided key/value pairs - Fires
DOMContentLoadedto triggerApp.init() - Returns
{ window, document }for assertions
import { loadPage } from '../helpers/loadPage.js';
// Load with no pre-existing data
const { document } = loadPage();
// Load with pre-seeded localStorage
const { document } = loadPage({ hh_theme: 'dark', hh_water: '5' });| File | Module tested | Tests |
|---|---|---|
theme.test.js |
ThemeModule | 4 |
banner.test.js |
BannerModule | 2 |
health-score.test.js |
HealthScoreModule | 3 |
dashboard.test.js |
DashboardModule | 5 |
period-tracker.test.js |
PeriodTrackerModule | 9 |
reminder.test.js |
ReminderModule | 4 |
nutrition.test.js |
NutritionModule | 5 |
exercise.test.js |
ExerciseModule | 6 |
mood.test.js |
MoodModule | 4 |
motivation.test.js |
MotivationModule | 6 |
| Total | 48 |
// tests/unit/dashboard.test.js
it('date rollover: set hh_last_date to yesterday, init, verify water resets to 0', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().slice(0, 10);
const { document } = loadPage({
hh_last_date: yesterdayStr,
hh_water: '5',
});
const waterEl = document.getElementById('water-count');
expect(waterEl.textContent.trim()).toBe('0');
});The app uses standard Web APIs available in all modern browsers:
| API | Used for |
|---|---|
localStorage |
All data persistence |
Notification |
Reminder browser notifications |
setInterval |
Reminder polling (60s resolution) |
| CSS Custom Properties | Design system / theming |
| CSS Grid | Responsive card layouts |
Date.toLocaleDateString |
Period date formatting |
Minimum recommended browsers: Chrome 80+, Firefox 75+, Safari 13+, Edge 80+
The app degrades gracefully when Notification permission is denied (in-app fallback) and when localStorage is unavailable (in-memory mode).