HydroFlux 0.0.1

This commit is contained in:
2026-02-04 20:56:57 +11:00
commit 5a8c661ce8
88 changed files with 7464 additions and 0 deletions

153
js/modules/fitness.js Normal file
View File

@@ -0,0 +1,153 @@
export class FitnessDashboard {
constructor(containerId) {
this.container = document.getElementById(containerId);
// Mock Data
this.data = {
steps: { current: 8432, goal: 10000 },
sleep: { current: 6.5, goal: 8 },
history: {
steps: [4500, 7200, 10500, 8900, 6000, 11200, 8432],
sleep: [5.5, 6.0, 7.5, 8.2, 5.0, 9.0, 6.5]
}
};
this.render();
// Delay animation to allow DOM paint
setTimeout(() => this.animate(), 100);
}
render() {
const stepPercent = Math.min((this.data.steps.current / this.data.steps.goal) * 100, 100);
const sleepPercent = Math.min((this.data.sleep.current / this.data.sleep.goal) * 100, 100);
// Ring Config
const center = 100;
const radiusOuter = 80;
const radiusInner = 55;
const circumOuter = 2 * Math.PI * radiusOuter;
const circumInner = 2 * Math.PI * radiusInner;
this.container.innerHTML = `
<div class="fitness-container">
<h2 class="section-title">DAILY ACTIVITY</h2>
<!-- Top: Concentric Rings -->
<div class="rings-wrapper">
<svg class="concentric-svg" viewBox="0 0 200 200">
<!-- Outer Track (Steps) -->
<circle class="ring-bg" cx="${center}" cy="${center}" r="${radiusOuter}" stroke-width="18"></circle>
<!-- Inner Track (Sleep) -->
<circle class="ring-bg" cx="${center}" cy="${center}" r="${radiusInner}" stroke-width="18"></circle>
<!-- Outer Progress (Steps - Cyan) -->
<circle class="ring-progress cyan" cx="${center}" cy="${center}" r="${radiusOuter}"
stroke-width="18"
stroke-dasharray="${circumOuter}"
stroke-dashoffset="${circumOuter}"
data-offset="${circumOuter - (stepPercent / 100) * circumOuter}"></circle>
<!-- Inner Progress (Sleep - Purple) -->
<circle class="ring-progress purple" cx="${center}" cy="${center}" r="${radiusInner}"
stroke-width="18"
stroke-dasharray="${circumInner}"
stroke-dashoffset="${circumInner}"
data-offset="${circumInner - (sleepPercent / 100) * circumInner}"></circle>
<!-- Icons/Center -->
<image href="https://fonts.gstatic.com/s/i/materialicons/bolt/v5/24px.svg" x="90" y="90" height="20" width="20" style="filter: invert(1); opacity: 0.5;" />
</svg>
<!-- Legend to explain the rings -->
<div class="rings-legend">
<div class="legend-item">
<span class="dot cyan"></span> STEPS
</div>
<div class="legend-item">
<span class="dot purple"></span> SLEEP
</div>
</div>
</div>
<!-- Middle: Device -->
<div class="device-card">
<div class="device-info">
<span class="device-name">TicWatch Pro 5</span>
<span class="device-status">Disconnected</span>
</div>
<button id="connect-watch-btn" class="connect-glow-btn">LINK</button>
</div>
<!-- Box 1: Steps (Current + History) -->
<div class="stat-card">
<div class="stat-header">
<div>
<span class="stat-label">STEPS</span>
<div class="stat-value">${this.data.steps.current}</div>
<div class="stat-sub">Goal: ${this.data.steps.goal}</div>
</div>
<div class="icon-box cyan-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
</div>
<div class="chart-divider"></div>
<div class="chart-container small">
${this.generateBars(this.data.history.steps, 12000, 'cyan')}
</div>
</div>
<!-- Box 2: Sleep (Current + History) -->
<div class="stat-card">
<div class="stat-header">
<div>
<span class="stat-label">SLEEP</span>
<div class="stat-value">${this.data.sleep.current}h</div>
<div class="stat-sub">Goal: ${this.data.sleep.goal}h</div>
</div>
<div class="icon-box purple-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</div>
</div>
<div class="chart-divider"></div>
<div class="chart-container small">
${this.generateBars(this.data.history.sleep, 10, 'purple')}
</div>
</div>
</div>
`;
this.attachEvents();
}
generateBars(data, max, colorClass) {
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
return data.map((val, index) => {
const height = Math.min((val / max) * 100, 100);
return `
<div class="chart-column">
<div class="chart-bar ${colorClass}" style="height: ${height}%"></div>
<span class="chart-day">${days[index]}</span>
</div>
`;
}).join('');
}
animate() {
this.container.querySelectorAll('.ring-progress').forEach(ring => {
ring.style.strokeDashoffset = ring.dataset.offset;
});
}
attachEvents() {
const btn = this.container.querySelector('#connect-watch-btn');
if (btn) {
btn.addEventListener('click', () => {
alert("Placeholder: This feature would connect to the WearOS API in the native app.");
});
}
}
}

102
js/modules/streak.js Normal file
View File

@@ -0,0 +1,102 @@
export class StreakTracker {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.STORAGE_KEY = 'hydroflux_streak';
this.quotes = [
"The only easy day was yesterday.",
"Discipline is doing what needs to be done, even if you don't want to.",
"Your future self is watching you right now through memories.",
"Pain is temporary. Quitting lasts forever.",
"Suffering is the currency of success.",
"Don't stop when you're tired. Stop when you're done.",
"You are stronger than your urges.",
"Focus on the goal, not the obstacle."
];
this.loadState();
this.render();
this.startTimer();
}
loadState() {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (saved) {
this.startDate = new Date(parseInt(saved));
} else {
this.startDate = new Date();
this.saveState();
}
}
saveState() {
localStorage.setItem(this.STORAGE_KEY, this.startDate.getTime().toString());
}
resetStreak() {
if (confirm("Are you sure you want to reset your streak?")) {
this.startDate = new Date();
this.saveState();
this.updateUI();
// Haptic Bad Feedback
if (navigator.vibrate) navigator.vibrate([100, 50, 100]);
}
}
getDuration() {
const now = new Date();
const diff = now - this.startDate;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
return { days, hours, minutes };
}
updateUI() {
const { days, hours, minutes } = this.getDuration();
const daysEl = this.container.querySelector('.streak-days');
const detailEl = this.container.querySelector('.streak-detail');
if (daysEl) daysEl.textContent = days;
if (detailEl) detailEl.textContent = `${hours}h ${minutes}m`;
}
startTimer() {
setInterval(() => this.updateUI(), 60000); // Update every minute
}
getRandomQuote() {
return this.quotes[Math.floor(Math.random() * this.quotes.length)];
}
render() {
this.container.innerHTML = `
<div class="streak-container">
<h2 class="section-title">ABSTINENCE STREAK</h2>
<div class="streak-counter">
<div class="streak-days glow-text">0</div>
<div class="streak-label">DAYS</div>
<div class="streak-detail">${0}h ${0}m</div>
</div>
<div class="quote-card">
"${this.getRandomQuote()}"
</div>
<button id="reset-streak-btn" class="danger-btn">
RESET STREAK
</button>
</div>
`;
this.updateUI();
this.container.querySelector('#reset-streak-btn').addEventListener('click', () => {
this.resetStreak();
});
}
}

184
js/modules/water.js Normal file
View File

@@ -0,0 +1,184 @@
export class WaterTracker {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.state = {
current: 0,
goal: 3000, // mL
bottleSize: 500, // mL
};
this.STORAGE_KEY = 'hydroflux_data';
this.loadState();
this.render();
this.attachEvents();
this.updateUI();
}
loadState() {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
this.state = { ...this.state, ...parsed };
}
}
saveState() {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.state));
this.updateUI();
}
addWater() {
this.state.current += this.state.bottleSize;
this.saveState();
if (navigator.vibrate) navigator.vibrate(50);
}
removeWater() {
this.state.current = Math.max(0, this.state.current - this.state.bottleSize);
this.saveState();
if (navigator.vibrate) navigator.vibrate(50);
}
setBottleSize(size) {
if (!size || size <= 0) return;
this.state.bottleSize = size;
this.saveState();
}
getPercentage() {
return Math.min(100, Math.max(0, (this.state.current / this.state.goal) * 100));
}
updateUI() {
// Update Text
const currentEl = this.container.querySelector('.water-count');
const percentageEl = this.container.querySelector('.water-percentage');
if (currentEl) currentEl.textContent = `${this.state.current} / ${this.state.goal} mL`;
if (percentageEl) percentageEl.textContent = `${Math.round(this.getPercentage())}%`;
// Update Wave Animation Height
const wave = this.container.querySelector('.wave');
if (wave) {
wave.style.top = `${100 - this.getPercentage()}%`;
}
}
render() {
this.container.innerHTML = `
<div class="water-tracker-container">
<!-- Circular Progress -->
<div class="circle-container">
<div class="water-circle">
<div class="wave"></div>
<div class="circle-content">
<span class="water-percentage">0%</span>
<span class="water-label">HYDRATION</span>
</div>
</div>
</div>
<!-- Stats Display -->
<div class="stats-row">
<span class="water-count">0 / 3000 mL</span>
</div>
<!-- Controls -->
<div class="controls-area">
<div class="bottle-selector">
<label>Bottle (mL):</label>
<input type="number" id="bottle-size-input" value="${this.state.bottleSize}" min="1" max="5000">
</div>
<div class="action-buttons">
<button id="remove-water-btn" class="icon-btn secondary" aria-label="Remove Drink">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
</svg>
</button>
<button id="add-water-btn" class="glow-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
DRINK
</button>
<!-- Notification Toggle -->
<button id="notify-btn" class="icon-btn" title="Enable Reminders">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
</button>
</div>
</div>
</div>
`;
this.checkNotificationStatus();
}
attachEvents() {
this.container.querySelector('#add-water-btn').addEventListener('click', () => {
this.addWater();
});
this.container.querySelector('#remove-water-btn').addEventListener('click', () => {
this.removeWater();
});
this.container.querySelector('#notify-btn').addEventListener('click', (e) => {
this.toggleNotifications(e.currentTarget);
});
const input = this.container.querySelector('#bottle-size-input');
input.addEventListener('change', (e) => {
this.setBottleSize(parseInt(e.target.value));
});
}
// --- Notification Logic ---
toggleNotifications(btn) {
if (!("Notification" in window)) {
alert("This browser does not support desktop notifications");
return;
}
if (Notification.permission === "granted") {
alert("Reminders are active! We'll check every hour.");
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
if (permission === "granted") {
this.startReminderLoop();
btn.style.color = "var(--primary-cyan)";
new Notification("HydroFlux", { body: "Smart Hydration Reminders Enabled!" });
}
});
}
}
checkNotificationStatus() {
if (Notification.permission === "granted") {
const btn = this.container.querySelector('#notify-btn');
if (btn) btn.style.color = "var(--primary-cyan)";
this.startReminderLoop();
}
}
startReminderLoop() {
// Clear existing to avoid duplicates
if (this.reminderInterval) clearInterval(this.reminderInterval);
// Check every minute if it's been > 1 hour since last drink
this.reminderInterval = setInterval(() => {
// Pseudo-logic check since we don't store timestamp in this simple version yet
// In a real app, you'd check this.state.lastDrinkTime
new Notification("HydroFlux Needs You", {
body: "Remember to drink water!",
icon: "/icon.png"
});
}, 3600000); // 1 Hour
}
}