HydroFlux 0.0.2
This commit is contained in:
319
Hydroflux/app/src/main/assets/js/modules/dashboard.js
Normal file
319
Hydroflux/app/src/main/assets/js/modules/dashboard.js
Normal file
@@ -0,0 +1,319 @@
|
||||
export class Dashboard {
|
||||
constructor(containerId, app) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.app = app;
|
||||
this.render();
|
||||
this.attachEvents();
|
||||
this.startTimers();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
// Data Retrieval (Keeping persistence)
|
||||
const waterData = JSON.parse(localStorage.getItem('hydroflux_data') || '{"current":1.2,"goal":3.0}');
|
||||
const stepsData = 8432;
|
||||
const goalData = 10000;
|
||||
const sleepHours = 7;
|
||||
const sleepMins = 20;
|
||||
|
||||
const goalsData = JSON.parse(localStorage.getItem('hydroflux_goals') || JSON.stringify([
|
||||
{ id: '1', text: 'Drink 3L of water', completed: true },
|
||||
{ id: '2', text: 'Walk 10K steps', completed: false },
|
||||
{ id: '3', text: 'Sleep 8 hours', completed: false },
|
||||
]));
|
||||
|
||||
const currentNote = localStorage.getItem('hydroflux_note_content') || 'Feeling hydrated today. Remember to add those gym sessions!';
|
||||
|
||||
// HTML from Single_File.html.txt (Body Content Only)
|
||||
// Adapted to use Template Literals for dynamic data
|
||||
this.container.innerHTML = `
|
||||
<div class="w-full max-w-md bg-white rounded-[3rem] shadow-2xl p-6 relative overflow-hidden mx-auto my-4">
|
||||
<!-- Background decoration -->
|
||||
<div class="absolute top-0 right-0 w-64 h-64 bg-blue-200 opacity-20 rounded-full blur-3xl -z-10"></div>
|
||||
<div class="absolute bottom-0 left-0 w-64 h-64 bg-purple-200 opacity-20 rounded-full blur-3xl -z-10"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col items-center gap-2 mb-6">
|
||||
<div class="flex items-center justify-between w-full px-4">
|
||||
<!-- Avatar -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center mb-1 shadow-lg">
|
||||
<div class="w-12 h-12 rounded-full bg-white bg-opacity-90 flex items-center justify-center text-sm font-semibold text-gray-700">
|
||||
JD
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-blue-600 font-medium">Level 14</span>
|
||||
</div>
|
||||
|
||||
<!-- Title and DateTime -->
|
||||
<div class="flex flex-col items-center flex-1">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-1">Hydro Flux</h1>
|
||||
<div id="currentDate" class="text-sm text-gray-600"></div>
|
||||
<div id="currentTime" class="text-sm text-gray-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Streak -->
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-5 h-5 fill-blue-500 text-blue-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
|
||||
</svg>
|
||||
<svg class="w-5 h-5 fill-blue-400 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-gray-700 ml-1">14 Days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Water Tracker -->
|
||||
<div class="water-card-n relative h-64 bg-gradient-to-b from-blue-50 to-blue-100 rounded-3xl overflow-hidden shadow-lg mb-6">
|
||||
<!-- Water fill -->
|
||||
<div id="waterFill" class="absolute bottom-0 left-0 right-0 water-container" style="height: ${(waterData.current / waterData.goal) * 100}%; background: linear-gradient(180deg, rgba(96, 165, 250, 0.8) 0%, rgba(59, 130, 246, 0.9) 100%);">
|
||||
<!-- Wave SVG -->
|
||||
<svg class="absolute top-0 left-0 w-full wave-animation" viewBox="0 0 1200 120" preserveAspectRatio="none" style="height: 60px; transform: translateY(-50%);">
|
||||
<path d="M0,50 Q300,10 600,50 T1200,50 L1200,120 L0,120 Z" fill="rgba(96, 165, 250, 0.5)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Add button -->
|
||||
<button id="addWaterBtn" class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-16 h-16 bg-blue-400 bg-opacity-50 hover:bg-opacity-70 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transition-all hover:scale-110 z-10">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Amount display -->
|
||||
<div class="absolute bottom-6 left-0 right-0 text-center z-10">
|
||||
<span id="waterAmount" class="text-2xl font-bold text-gray-700">${parseFloat(waterData.current).toFixed(1)}L / ${parseFloat(waterData.goal).toFixed(1)}L</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid 1: Steps and Strava -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<!-- Steps Tracker -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Steps Tracker</h3>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative w-32 h-32">
|
||||
<svg class="transform -rotate-90 w-32 h-32">
|
||||
<circle cx="64" cy="64" r="45" stroke="#E5E7EB" stroke-width="8" fill="none" />
|
||||
<circle id="stepsCircle" cx="64" cy="64" r="45" stroke="#60A5FA" stroke-width="8" fill="none" stroke-linecap="round" style="transition: stroke-dashoffset 0.5s;" />
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span class="text-2xl font-bold text-gray-800">${stepsData.toLocaleString()}</span>
|
||||
<span class="text-xs text-gray-500">steps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strava Integration -->
|
||||
<div class="bg-gradient-to-br from-orange-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Strava Integration</h3>
|
||||
<div class="flex flex-col items-center justify-center py-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl flex items-center justify-center mb-3 shadow-lg transform rotate-12">
|
||||
<span class="text-3xl font-bold text-white transform -rotate-12">S</span>
|
||||
</div>
|
||||
<span class="text-lg font-bold text-gray-800">5.2km Run</span>
|
||||
<span class="text-xs text-gray-500 mt-1">Last Sync: 2h ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep Tracker -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md mb-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Sleep Tracker</h3>
|
||||
<div class="h-24 mb-3">
|
||||
<svg id="sleepChart" class="w-full h-full" viewBox="0 0 300 100" preserveAspectRatio="none">
|
||||
<polyline id="sleepLine" points="" fill="none" stroke="#60A5FA" stroke-width="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xl font-bold text-gray-800">${sleepHours}h ${sleepMins}m</span>
|
||||
<span class="text-sm text-blue-600 font-medium">Light Sleep</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid 2: Goals and Notes -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Goals -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Goals</h3>
|
||||
<div class="space-y-3" id="goalsList">
|
||||
<!-- Injected via JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Notes</h3>
|
||||
<textarea id="notesInput" placeholder="Add your notes here..." class="w-full h-24 bg-white bg-opacity-50 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-400 border-none outline-none resize-none">${currentNote}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.updateDynamicElements(stepsData, goalData);
|
||||
this.renderGoals(goalsData);
|
||||
}
|
||||
|
||||
updateDynamicElements(steps, goal) {
|
||||
// Steps Circle Logic
|
||||
const percentage = Math.min((steps / goal) * 100, 100);
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
||||
const circle = this.container.querySelector('#stepsCircle');
|
||||
if (circle) {
|
||||
circle.style.strokeDasharray = circumference;
|
||||
circle.style.strokeDashoffset = strokeDashoffset;
|
||||
}
|
||||
|
||||
// Sleep Chart Logic
|
||||
const sleepData = [30, 35, 25, 40, 55, 45, 60, 75, 70, 80, 65, 55, 50, 45, 40];
|
||||
const chartWidth = 300;
|
||||
const chartHeight = 100;
|
||||
const points = sleepData.map((value, index) => {
|
||||
const x = (index / (sleepData.length - 1)) * chartWidth;
|
||||
const y = chartHeight - (value / 100) * chartHeight;
|
||||
return x + ',' + y;
|
||||
}).join(' ');
|
||||
const line = this.container.querySelector('#sleepLine');
|
||||
if (line) line.setAttribute('points', points);
|
||||
}
|
||||
|
||||
renderGoals(goalsData) {
|
||||
const list = this.container.querySelector('#goalsList');
|
||||
if (!list) return;
|
||||
list.innerHTML = goalsData.map(g => `
|
||||
<button class="goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all ${g.completed ? 'bg-blue-500 text-white shadow-lg' : 'bg-blue-100 text-gray-700 hover:bg-blue-200'}" data-id="${g.id}" data-completed="${g.completed}">
|
||||
<div class="w-5 h-5 rounded-full flex items-center justify-center ${g.completed ? 'bg-white bg-opacity-30' : 'bg-white'}">
|
||||
${g.completed ? '<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>' : ''}
|
||||
</div>
|
||||
<span class="text-sm font-medium">${g.text}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
// Re-attach listeners for goals
|
||||
list.querySelectorAll('.goal-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.dataset.id;
|
||||
const goal = goalsData.find(g => g.id === id);
|
||||
if (goal) {
|
||||
goal.completed = !goal.completed;
|
||||
localStorage.setItem('hydroflux_goals', JSON.stringify(goalsData));
|
||||
this.renderGoals(goalsData); // Re-render goals only
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
const self = this; // Capture this
|
||||
|
||||
// Navigation Events (Widget Clicks)
|
||||
// Water - Targeting the wrapper div mostly, ensuring specific targets don't block
|
||||
const waterCard = this.container.querySelector('.water-card-n');
|
||||
if (waterCard) {
|
||||
waterCard.style.cursor = 'pointer';
|
||||
waterCard.addEventListener('click', (e) => {
|
||||
// IMPORTANT: Do NOT navigate if clicking the 'Add Water' button or its SVG children
|
||||
if (e.target.closest('#addWaterBtn')) {
|
||||
// Do nothing, the add listener below handles it
|
||||
return;
|
||||
}
|
||||
this.app.navigateTo('water-detail');
|
||||
});
|
||||
}
|
||||
|
||||
// Notes
|
||||
const notesCard = this.container.querySelector('#notesInput')?.closest('.rounded-2xl');
|
||||
if (notesCard) {
|
||||
// Make the whole card clickable, but maybe not the textarea itself if we want to allow quick edits?
|
||||
// User requested "click on notes we get a fullscreen pop", so presumably we redirect immediately.
|
||||
// Or we make the header/container clickable.
|
||||
// Let's make the textarea readonly in dashboard view OR just redirect on focus?
|
||||
// "click on notes we get a fullscreen pop" -> redirect on click anywhere on card
|
||||
const ta = this.container.querySelector('#notesInput');
|
||||
if (ta) ta.setAttribute('readonly', 'true'); // Make it read-only on dashboard to force click-through
|
||||
|
||||
notesCard.style.cursor = 'pointer';
|
||||
notesCard.addEventListener('click', () => {
|
||||
this.app.navigateTo('notes-detail');
|
||||
});
|
||||
}
|
||||
const stepsCard = this.container.querySelector('#stepsCircle')?.closest('.rounded-2xl');
|
||||
if (stepsCard) {
|
||||
stepsCard.style.cursor = 'pointer';
|
||||
stepsCard.addEventListener('click', () => this.app.navigateTo('fitness-detail'));
|
||||
}
|
||||
|
||||
// Strava
|
||||
const stravaCard = this.container.querySelector('.from-orange-50');
|
||||
if (stravaCard) {
|
||||
stravaCard.style.cursor = 'pointer';
|
||||
stravaCard.addEventListener('click', () => this.app.navigateTo('fitness-detail'));
|
||||
}
|
||||
|
||||
// Sleep
|
||||
const sleepCard = this.container.querySelector('#sleepChart')?.closest('.rounded-2xl');
|
||||
if (sleepCard) {
|
||||
sleepCard.style.cursor = 'pointer';
|
||||
sleepCard.addEventListener('click', () => this.app.navigateTo('sleep-detail'));
|
||||
}
|
||||
|
||||
// Goals
|
||||
const goalsCard = this.container.querySelector('#goalsList')?.closest('.rounded-2xl');
|
||||
if (goalsCard) {
|
||||
// Only navigate if not clicking a specialized button, but goals list is buttons...
|
||||
// Maybe we add a "View All" or just make the header clickable?
|
||||
// For now, let's make the header clickable
|
||||
const header = goalsCard.querySelector('h3');
|
||||
if (header) {
|
||||
header.innerHTML += ' <span class="text-xs text-blue-500 float-right cursor-pointer">View All →</span>';
|
||||
header.addEventListener('click', () => this.app.navigateTo('goals-detail'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Water Add
|
||||
const addBtn = this.container.querySelector('#addWaterBtn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Stop bubble to card click
|
||||
const data = JSON.parse(localStorage.getItem('hydroflux_data') || '{"current":1.2,"goal":3.0}');
|
||||
if (data.current < data.goal) {
|
||||
data.current = Math.min(data.current + 0.25, data.goal);
|
||||
localStorage.setItem('hydroflux_data', JSON.stringify(data));
|
||||
|
||||
// Update Display without full re-render
|
||||
const percentage = (data.current / data.goal) * 100;
|
||||
const fill = this.container.querySelector('#waterFill');
|
||||
const amount = this.container.querySelector('#waterAmount');
|
||||
if (fill) fill.style.height = percentage + '%';
|
||||
if (amount) amount.textContent = data.current.toFixed(1) + 'L / ' + data.goal.toFixed(1) + 'L';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notes Save
|
||||
const noteArea = this.container.querySelector('#notesInput');
|
||||
if (noteArea) {
|
||||
noteArea.addEventListener('input', (e) => {
|
||||
localStorage.setItem('hydroflux_note_content', e.target.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startTimers() {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
const dateEl = this.container.querySelector('#currentDate');
|
||||
const timeEl = this.container.querySelector('#currentTime');
|
||||
if (dateEl) dateEl.textContent = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
if (timeEl) timeEl.textContent = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }) + ' AEDT';
|
||||
};
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user