diff --git a/Hydroflux/.idea/vcs.xml b/Hydroflux/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Hydroflux/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Hydroflux/REPLICATION_GUIDE.md b/Hydroflux/REPLICATION_GUIDE.md new file mode 100644 index 0000000..7bb4b55 --- /dev/null +++ b/Hydroflux/REPLICATION_GUIDE.md @@ -0,0 +1,536 @@ +# Hydro Flux Dashboard - Complete Replication Guide + +## Overview +A mobile-first health tracking dashboard called "Hydro Flux" that tracks water intake, steps, sleep, Strava integration, goals, and notes. Built with React, TypeScript, Tailwind CSS, and Motion (Framer Motion). + +## Design Specifications + +### Color Palette +- Primary Blue: #60A5FA (blue-400) +- Secondary Blue: #3B82F6 (blue-500) +- Light Blue: #DBEAFE (blue-50) +- Orange (Strava): #EA580C (orange-600) +- Gray Background: Linear gradient from gray-100 to gray-200 +- White: #FFFFFF +- Text Primary: #1F2937 (gray-800) +- Text Secondary: #6B7280 (gray-500) + +### Layout +- Mobile-first design (max-width: 28rem / 448px) +- Rounded container with 3rem border radius +- White background with shadow-2xl +- Padding: 1.5rem (6 units) +- Component spacing: 1rem gap (4 units) + +### Typography +- Headers: font-bold +- Body: font-medium +- Small text: text-sm, text-xs + +## Dependencies Required + +```json +{ + "recharts": "2.15.2", + "motion": "12.23.24", + "lucide-react": "0.487.0" +} +``` + +Import Motion using: `import { motion } from 'motion/react'` + +## File Structure + +``` +/src/app/ + ├── App.tsx + └── components/ + ├── Header.tsx + ├── WaterTracker.tsx + ├── StepsTracker.tsx + ├── StravaIntegration.tsx + ├── SleepTracker.tsx + ├── Goals.tsx + └── Notes.tsx +``` + +## Complete Component Code + +### 1. /src/app/App.tsx + +```tsx +import { useState, useEffect } from 'react'; +import { Header } from './components/Header'; +import { WaterTracker } from './components/WaterTracker'; +import { StepsTracker } from './components/StepsTracker'; +import { StravaIntegration } from './components/StravaIntegration'; +import { SleepTracker } from './components/SleepTracker'; +import { Goals } from './components/Goals'; +import { Notes } from './components/Notes'; + +function App() { + const [waterAmount, setWaterAmount] = useState(1.2); + const [currentDate, setCurrentDate] = useState(''); + const [currentTime, setCurrentTime] = useState(''); + const [note, setNote] = useState('Feeling hydrated today. Remember to add those gym sessions!'); + const [goals, setGoals] = useState([ + { 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 }, + ]); + + // Sleep data for the chart + const sleepData = [ + { value: 30 }, { value: 35 }, { value: 25 }, { value: 40 }, + { value: 55 }, { value: 45 }, { value: 60 }, { value: 75 }, + { value: 70 }, { value: 80 }, { value: 65 }, { value: 55 }, + { value: 50 }, { value: 45 }, { value: 40 }, + ]; + + useEffect(() => { + const updateDateTime = () => { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + const timeStr = now.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }) + ' AEDT'; + setCurrentDate(dateStr); + setCurrentTime(timeStr); + }; + + updateDateTime(); + const interval = setInterval(updateDateTime, 1000); + return () => clearInterval(interval); + }, []); + + const handleAddWater = () => { + setWaterAmount((prev) => Math.min(prev + 0.25, 3.0)); + }; + + const handleToggleGoal = (id: string) => { + setGoals((prev) => + prev.map((goal) => + goal.id === id ? { ...goal, completed: !goal.completed } : goal + ) + ); + }; + + return ( +
+
+ {/* Subtle background decoration */} +
+
+ +
+ + + +
+ + +
+ +
+ +
+ +
+ + +
+
+
+ ); +} + +export default App; +``` + +### 2. /src/app/components/Header.tsx + +```tsx +import { Droplet } from 'lucide-react'; + +interface HeaderProps { + level: number; + streakDays: number; + currentDate: string; + currentTime: string; +} + +export function Header({ level, streakDays, currentDate, currentTime }: HeaderProps) { + return ( +
+
+
+
+
+ JD +
+
+ Level {level} +
+ +
+

Hydro Flux

+
{currentDate}
+
{currentTime}
+
+ +
+ + + {streakDays} Days +
+
+
+ ); +} +``` + +### 3. /src/app/components/WaterTracker.tsx + +```tsx +import { Plus } from 'lucide-react'; +import { motion } from 'motion/react'; + +interface WaterTrackerProps { + currentAmount: number; + targetAmount: number; + onAddWater: () => void; +} + +export function WaterTracker({ currentAmount, targetAmount, onAddWater }: WaterTrackerProps) { + const percentage = (currentAmount / targetAmount) * 100; + + return ( +
+ {/* Water wave */} + + {/* Wave effect */} + + + + + + {/* Add button */} + + + {/* Amount display */} +
+ + {currentAmount.toFixed(1)}L / {targetAmount.toFixed(1)}L + +
+
+ ); +} +``` + +### 4. /src/app/components/StepsTracker.tsx + +```tsx +interface StepsTrackerProps { + steps: number; + goal: number; +} + +export function StepsTracker({ steps, goal }: StepsTrackerProps) { + const percentage = Math.min((steps / goal) * 100, 100); + const circumference = 2 * Math.PI * 45; + const strokeDashoffset = circumference - (percentage / 100) * circumference; + + return ( +
+

Steps Tracker

+
+
+ + + + +
+ {steps.toLocaleString()} + steps +
+
+
+
+ ); +} +``` + +### 5. /src/app/components/StravaIntegration.tsx + +```tsx +interface StravaIntegrationProps { + distance: number; + lastSync: string; +} + +export function StravaIntegration({ distance, lastSync }: StravaIntegrationProps) { + return ( +
+

Strava Integration

+
+
+ S +
+ {distance}km Run + Last Sync: {lastSync} +
+
+ ); +} +``` + +### 6. /src/app/components/SleepTracker.tsx + +```tsx +import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts'; + +interface SleepTrackerProps { + hours: number; + minutes: number; + sleepType: string; + data: { value: number }[]; +} + +export function SleepTracker({ hours, minutes, sleepType, data }: SleepTrackerProps) { + return ( +
+

Sleep Tracker

+
+ + + + + + +
+
+ + {hours}h {minutes}m + + {sleepType} +
+
+ ); +} +``` + +### 7. /src/app/components/Goals.tsx + +```tsx +import { Check } from 'lucide-react'; + +interface Goal { + id: string; + text: string; + completed: boolean; +} + +interface GoalsProps { + goals: Goal[]; + onToggleGoal: (id: string) => void; +} + +export function Goals({ goals, onToggleGoal }: GoalsProps) { + return ( +
+

Goals

+
+ {goals.map((goal) => ( + + ))} +
+
+ ); +} +``` + +### 8. /src/app/components/Notes.tsx + +```tsx +interface NotesProps { + note: string; + onNoteChange: (note: string) => void; +} + +export function Notes({ note, onNoteChange }: NotesProps) { + return ( +
+

Notes

+ +
+
+
+ + + + diff --git a/Hydroflux/app/src/main/assets/css/style.css b/Hydroflux/app/src/main/assets/css/style.css index ac54b5a..1a691bc 100644 --- a/Hydroflux/app/src/main/assets/css/style.css +++ b/Hydroflux/app/src/main/assets/css/style.css @@ -1,955 +1,1133 @@ -:root { - /* Futuristic Palette */ - --bg-dark: #050508; - --bg-panel: rgba(20, 20, 35, 0.4); - --primary-cyan: #00f3ff; - --secondary-purple: #bc13fe; - --text-main: #e0e0e0; - --text-muted: #8a8a9b; - --glass-border: rgba(255, 255, 255, 0.08); - --neon-shadow: 0 0 10px rgba(0, 243, 255, 0.5); - - /* Spacing */ - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - - /* Font */ - --font-heading: 'Orbitron', sans-serif; - --font-body: 'Outfit', sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; - /* Remove mobile tap highlight */ -} - -body { - background-color: var(--bg-dark); - color: var(--text-main); - font-family: var(--font-body); - height: 100vh; - overflow: hidden; - background-image: - radial-gradient(circle at 10% 20%, rgba(188, 19, 254, 0.1) 0%, transparent 40%), - radial-gradient(circle at 90% 80%, rgba(0, 243, 255, 0.08) 0%, transparent 40%); -} - -#app { - display: flex; - flex-direction: column; - height: 100%; -} - -/* Typography */ -h1, -h2, -h3 { - font-family: var(--font-heading); - letter-spacing: 1px; -} - -.glow-text { - text-shadow: 0 0 8px rgba(0, 243, 255, 0.3); -} - -.highlight { - color: var(--primary-cyan); -} - -/* Glassmorphism Utilities */ -.glass-header { - background: rgba(5, 5, 8, 0.7); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - padding: var(--spacing-md); - border-bottom: 1px solid var(--glass-border); - display: flex; - justify-content: space-between; - align-items: center; - position: sticky; - top: 0; - z-index: 10; -} - -.glass-panel { - background: var(--bg-panel); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: 1px solid var(--glass-border); - border-radius: 20px; - padding: var(--spacing-lg); - margin: var(--spacing-md); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); -} - -.glass-nav { - background: rgba(10, 10, 16, 0.8); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-top: 1px solid var(--glass-border); - padding: var(--spacing-md) var(--spacing-lg); - display: flex; - justify-content: space-around; - position: fixed; - bottom: 0; - width: 100%; - padding-bottom: max(16px, env(safe-area-inset-bottom)); - /* Safe area for iOS */ -} - -/* Content Area */ -#main-content { - flex: 1; - overflow-y: auto; - padding-bottom: 80px; - /* Space for nav */ -} - -/* Interactive Elements */ -.nav-btn { - background: none; - border: none; - color: var(--text-muted); - padding: 10px; - border-radius: 50%; - transition: all 0.3s ease; - cursor: pointer; -} - -.nav-btn.active { - color: var(--primary-cyan); - background: rgba(0, 243, 255, 0.1); - box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); -} - -.nav-btn svg { - display: block; -} - -/* Animations */ -@keyframes pulse-glow { - 0% { - box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); - } - - 50% { - box-shadow: 0 0 20px rgba(0, 243, 255, 0.8); - } - - 100% { - box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); - } -} - -/* --- Water Tracker Module Styles --- */ - -.water-tracker-container { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-lg); -} - -.circle-container { - position: relative; - width: 250px; - height: 250px; - border-radius: 50%; - border: 4px solid var(--bg-panel); - box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); - overflow: hidden; - /* Clips the wave */ - background: rgba(0, 0, 0, 0.3); -} - -.water-circle { - width: 100%; - height: 100%; - position: relative; -} - -/* Wave Animation */ -.wave { - position: absolute; - top: 100%; - /* Dynamic */ - left: -50%; - width: 200%; - height: 200%; - background: rgba(0, 243, 255, 0.4); - border-radius: 40%; - animation: rotate 6s linear infinite; - transition: top 1s ease-in-out; -} - -.wave::before { - content: ""; - position: absolute; - top: 5px; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 243, 255, 0.3); - /* Lighter top layer */ - border-radius: 40%; - animation: rotate 10s linear infinite reverse; -} - -@keyframes rotate { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -.circle-content { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 2; - /* Above wave */ - pointer-events: none; -} - -.water-percentage { - font-family: var(--font-heading); - font-size: 3rem; - font-weight: 700; - color: #fff; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); -} - -.water-label { - font-size: 0.8rem; - letter-spacing: 2px; - color: rgba(255, 255, 255, 0.7); - margin-top: -5px; -} - -.stats-row { - font-family: var(--font-heading); - font-size: 1.2rem; - color: var(--primary-cyan); -} - -/* Controls */ -.controls-area { - width: 100%; - display: flex; - flex-direction: column; - gap: var(--spacing-md); - align-items: center; -} - -.bottle-selector { - display: flex; - align-items: center; - gap: 10px; - font-size: 0.9rem; - color: var(--text-muted); -} - -.bottle-selector input { - background: rgba(0, 0, 0, 0.3); - border: 1px solid var(--primary-cyan); - color: var(--primary-cyan); - padding: 10px; - border-radius: 12px; - font-family: var(--font-heading); - width: 80px; - text-align: center; - font-size: 1.1rem; - outline: none; - box-shadow: 0 0 10px rgba(0, 243, 255, 0.1); -} - -.bottle-selector input:focus { - box-shadow: 0 0 15px rgba(0, 243, 255, 0.3); -} - -.action-buttons { - display: flex; - align-items: center; - gap: 20px; - margin-top: 10px; -} - -.icon-btn { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - color: var(--text-muted); - width: 60px; - height: 60px; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.icon-btn.secondary { - border-color: rgba(255, 50, 50, 0.3); - color: #ff4d4d; -} - -.icon-btn:active { - transform: scale(0.9); -} - -.glow-btn { - position: relative; - background: rgba(0, 243, 255, 0.1); - border: 1px solid var(--primary-cyan); - color: var(--primary-cyan); - font-family: var(--font-heading); - font-size: 1.1rem; - font-weight: 700; - padding: 0 40px; - height: 60px; - border-radius: 30px; - cursor: pointer; - display: flex; - align-items: center; - gap: 12px; - box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 20px rgba(0, 243, 255, 0.1); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - text-transform: uppercase; - letter-spacing: 2px; - overflow: hidden; -} - -.glow-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(0, 243, 255, 0.4), transparent); - transition: 0.5s; -} - -.glow-btn:hover { - background: rgba(0, 243, 255, 0.2); - box-shadow: 0 0 30px rgba(0, 243, 255, 0.6); - color: #fff; - text-shadow: 0 0 8px rgba(0, 243, 255, 0.8); -} - -.glow-btn:hover::before { - left: 100%; -} - -.glow-btn:active { - transform: scale(0.98); -} - -.glow-btn svg { - filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8)); -} - -/* --- Streak Module Styles --- */ -.streak-container { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-lg); - text-align: center; -} - -.section-title { - font-size: 1.2rem; - color: var(--secondary-purple); - letter-spacing: 4px; - margin-bottom: 20px; -} - -.streak-counter { - background: radial-gradient(circle, rgba(188, 19, 254, 0.1) 0%, transparent 70%); - padding: 40px; - border-radius: 50%; - margin-bottom: 20px; -} - -.streak-days { - font-family: var(--font-heading); - font-size: 6rem; - color: white; - line-height: 1; - text-shadow: 0 0 20px rgba(188, 19, 254, 0.6); -} - -.streak-label { - font-size: 1.5rem; - color: var(--text-muted); - letter-spacing: 5px; -} - -.streak-detail { - font-size: 1rem; - color: var(--primary-cyan); - margin-top: 10px; -} - -.quote-card { - background: rgba(255, 255, 255, 0.05); - border-left: 3px solid var(--primary-cyan); - padding: 20px; - font-style: italic; - color: #dedede; - margin: 20px 0; - line-height: 1.6; - max-width: 300px; -} - -.danger-btn { - position: relative; - background: rgba(255, 50, 50, 0.1); - border: 1px solid #ff4d4d; - color: #ff4d4d; - font-family: var(--font-heading); - padding: 30px 20px 15px; - /* Increased top padding for camera cutout */ - border-radius: 30px; - cursor: pointer; - font-size: 1rem; - letter-spacing: 2px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - margin-top: 30px; - box-shadow: 0 0 10px rgba(255, 50, 50, 0.2), inset 0 0 20px rgba(255, 50, 50, 0.1); - text-transform: uppercase; - font-weight: 700; -} - -.danger-btn:hover { - background: rgba(255, 50, 50, 0.2); - box-shadow: 0 0 30px rgba(255, 50, 50, 0.6); - color: #fff; - text-shadow: 0 0 8px rgba(255, 50, 50, 0.8); - transform: translateY(-2px); -} - -.danger-btn:active { - transform: scale(0.95); -} - -/* --- Connect Button (Neon Cyan Variant) --- */ -.connect-glow-btn { - position: relative; - background: rgba(0, 243, 255, 0.1); - border: 1px solid var(--primary-cyan); - color: var(--primary-cyan); - font-family: var(--font-heading); - padding: 8px 24px; - border-radius: 20px; - cursor: pointer; - font-size: 0.9rem; - letter-spacing: 1px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 10px rgba(0, 243, 255, 0.1); - text-transform: uppercase; - font-weight: 700; -} - -.connect-glow-btn:hover { - background: rgba(0, 243, 255, 0.2); - box-shadow: 0 0 20px rgba(0, 243, 255, 0.6); - color: #fff; - text-shadow: 0 0 5px rgba(0, 243, 255, 0.8); - transform: translateY(-1px); -} - -.connect-glow-btn:active { - transform: scale(0.95); -} - -/* --- Concentric Rings --- */ -.rings-wrapper { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 20px; - background: radial-gradient(circle, rgba(255, 255, 255, 0.02) 0%, transparent 70%); - padding: 20px; - border-radius: 50%; -} - -.concentric-svg { - width: 200px; - height: 200px; - transform: rotate(-90deg); -} - -.ring-bg { - fill: none; - stroke: rgba(255, 255, 255, 0.05); - /* Proper track color */ -} - -.ring-progress { - fill: none; - stroke-linecap: round; - transition: stroke-dashoffset 1.5s ease-in-out; -} - -.ring-progress.cyan { - stroke: var(--primary-cyan); - filter: drop-shadow(0 0 5px var(--primary-cyan)); -} - -.ring-progress.purple { - stroke: var(--secondary-purple); - filter: drop-shadow(0 0 5px var(--secondary-purple)); -} - -.rings-legend { - display: flex; - gap: 20px; - margin-top: 15px; -} - -.legend-item { - font-size: 0.8rem; - color: var(--text-muted); - display: flex; - align-items: center; - gap: 8px; - letter-spacing: 1px; -} - -.dot { - width: 8px; - height: 8px; - border-radius: 50%; -} - -.dot.cyan { - background: var(--primary-cyan); - box-shadow: 0 0 5px var(--primary-cyan); -} - -.dot.purple { - background: var(--secondary-purple); - box-shadow: 0 0 5px var(--secondary-purple); -} - -/* --- Combined Stat Card & Charts --- */ -.stat-header { - display: flex; - justify-content: space-between; - align-items: flex-start; -} - -.stat-label { - font-size: 0.8rem; - color: var(--text-muted); - letter-spacing: 2px; - margin-bottom: 5px; - display: block; -} - -.stat-value { - font-family: var(--font-heading); - font-size: 2rem; - color: #fff; - line-height: 1; -} - -.stat-sub { - font-size: 0.8rem; - color: rgba(255, 255, 255, 0.5); - margin-top: 5px; -} - -.icon-box { - width: 40px; - height: 40px; - border-radius: 12px; - display: flex; - justify-content: center; - align-items: center; -} - -.cyan-box { - background: rgba(0, 243, 255, 0.1); - color: var(--primary-cyan); -} - -.purple-box { - background: rgba(188, 19, 254, 0.1); - color: var(--secondary-purple); -} - -.chart-divider { - height: 1px; - background: rgba(255, 255, 255, 0.1); - margin: 20px 0; -} - -/* Re-using chart styles but refining for the box */ -.chart-container.small { - height: 100px; - margin-top: 0; - border-bottom: none; - padding-bottom: 0; - display: flex; - justify-content: space-between; - align-items: flex-end; -} - -.chart-column { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -.chart-bar { - width: 6px; - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; - transition: height 1s ease-out; -} - -.chart-bar.cyan { - background: var(--primary-cyan); - box-shadow: 0 0 5px rgba(0, 243, 255, 0.3); -} - -.chart-bar.purple { - background: var(--secondary-purple); - box-shadow: 0 0 5px rgba(188, 19, 254, 0.3); -} - -.chart-day { - color: var(--text-muted); -} - -/* --- Calendar Module Styles --- */ -.calendar-wrapper { - background: rgba(255, 255, 255, 0.03); - border-radius: 20px; - padding: 20px; - width: 100%; - max-width: 350px; - margin-top: 20px; - border: 1px solid var(--glass-border); -} - -.calendar-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; - font-family: var(--font-heading); - color: var(--text-main); -} - -.cal-nav-btn { - background: none; - border: none; - color: var(--text-muted); - font-size: 1.2rem; - cursor: pointer; - padding: 5px 10px; - transition: color 0.3s; -} - -.cal-nav-btn:hover { - color: var(--primary-cyan); -} - -.calendar-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 8px; - text-align: center; -} - -.cal-day-label { - color: var(--text-muted); - font-size: 0.7rem; - margin-bottom: 5px; - font-weight: 600; -} - -.cal-day { - aspect-ratio: 1; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: rgba(255, 255, 255, 0.02); - color: rgba(255, 255, 255, 0.3); - font-size: 0.9rem; - position: relative; - font-family: var(--font-body); -} - -.cal-day.active { - background: rgba(188, 19, 254, 0.2); - color: #fff; - box-shadow: 0 0 10px rgba(188, 19, 254, 0.2); - border: 1px solid rgba(188, 19, 254, 0.4); -} - -.cal-day.start-date { - background: rgba(0, 243, 255, 0.2); - border-color: var(--primary-cyan); - box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); -} - -.cal-day.today::after { - content: ''; - position: absolute; - bottom: 4px; - width: 4px; - height: 4px; - background: #fff; - border-radius: 50%; -} - -/* --- Modal Styles --- */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(5px); - z-index: 100; - display: flex; - justify-content: center; - align-items: center; - animation: fadeIn 0.3s ease; -} - -.modal-content { - width: 90%; - max-width: 400px; - background: rgba(10, 10, 20, 0.95); - /* Darker for readability */ - border: 1px solid var(--primary-cyan); - box-shadow: 0 0 30px rgba(0, 243, 255, 0.2); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - border-bottom: 1px solid var(--glass-border); - padding-bottom: 10px; -} - -.icon-btn-small { - background: none; - border: none; - color: var(--text-muted); - font-size: 1.5rem; - cursor: pointer; -} - -.setting-group { - margin-bottom: 20px; -} - -.setting-group label { - display: block; - color: var(--text-muted); - margin-bottom: 8px; - font-size: 0.9rem; - letter-spacing: 1px; -} - -.glow-input { - width: 100%; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - color: #fff; - padding: 15px; - border-radius: 12px; - font-size: 1.1rem; - font-family: var(--font-heading); - outline: none; - transition: all 0.3s; -} - -.glow-input:focus { - border-color: var(--primary-cyan); - box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); -} - -.full-width { - width: 100%; - justify-content: center; - margin-top: 10px; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -/* --- Fitness Schedule Styles --- */ -.workout-card { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - border-radius: 20px; - padding: 20px; - width: 100%; - margin-bottom: 20px; - position: relative; - overflow: hidden; -} - -.glow-card { - box-shadow: 0 0 20px rgba(0, 243, 255, 0.1); - border-color: rgba(0, 243, 255, 0.3); -} - -.card-header { - margin-bottom: 20px; -} - -.day-badge { - font-size: 0.8rem; - color: var(--text-muted); - letter-spacing: 2px; - display: block; - margin-bottom: 5px; -} - -.workout-type { - font-family: var(--font-heading); - font-size: 1.8rem; - line-height: 1.2; - text-transform: uppercase; -} - -.exercise-list { - display: flex; - flex-direction: column; - gap: 15px; -} - -.exercise-item { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - padding-bottom: 10px; -} - -.exercise-item:last-child { - border-bottom: none; -} - -.ex-name { - font-size: 1rem; - color: #e0e0e0; -} - -.ex-meta { - font-family: var(--font-heading); - color: var(--primary-cyan); - font-size: 0.9rem; - text-align: right; -} - -.ex-note { - font-family: var(--font-body); - font-size: 0.8rem; - color: var(--text-muted); - font-weight: normal; -} - -.subsection-title { - font-size: 1rem; - color: var(--text-muted); - letter-spacing: 2px; - margin-bottom: 15px; - margin-top: 10px; -} - -.schedule-list { - width: 100%; -} - -.schedule-row { - display: flex; - justify-content: space-between; - padding: 12px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - font-size: 0.9rem; -} - -.sch-day { - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 1px; -} - -.sch-type { - font-weight: 600; -}/* --- New Fitness Stats Display --- */ -.rings-stats { - display: flex; - justify-content: center; - gap: 40px; - margin-top: 20px; - width: 100%; -} - -.stat-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; -} - -.stat-item.cyan .stat-number { - color: var(--primary-cyan); - text-shadow: 0 0 10px rgba(0, 243, 255, 0.4); -} - -.stat-item.purple .stat-number { - color: var(--secondary-purple); - text-shadow: 0 0 10px rgba(188, 19, 254, 0.4); -} - -.stat-label-small { - font-size: 0.75rem; - color: var(--text-muted); - letter-spacing: 2px; - display: flex; - align-items: center; - gap: 6px; -} - -.stat-number { - font-family: var(--font-heading); - font-size: 1.8rem; - font-weight: 700; -} - \ No newline at end of file + /* --- DEPRECATED GLOBAL STYLES (CLEARED FOR TAILWIND LAYOUT) --- */ + /* + :root { ... } + body { ... } + #app { ... } + */ + /* Keeping Font definitions for reference if needed context */ + :root { + --font-heading: 'Orbitron', sans-serif; + --font-body: 'Outfit', sans-serif; + } + + * { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; + } + + /* Global Background handled by Tailwind Utility Classes on body */ + + /* --- Water Tracker Module Styles --- */ + + .water-tracker-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); + } + + .circle-container { + position: relative; + width: 250px; + height: 250px; + border-radius: 50%; + border: 4px solid var(--bg-panel); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + overflow: hidden; + /* Clips the wave */ + background: rgba(0, 0, 0, 0.3); + } + + .water-circle { + width: 100%; + height: 100%; + position: relative; + } + + /* Wave Animation */ + .wave { + position: absolute; + top: 100%; + /* Dynamic */ + left: -50%; + width: 200%; + height: 200%; + background: rgba(0, 243, 255, 0.4); + border-radius: 40%; + animation: rotate 6s linear infinite; + transition: top 1s ease-in-out; + } + + .wave::before { + content: ""; + position: absolute; + top: 5px; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 243, 255, 0.3); + /* Lighter top layer */ + border-radius: 40%; + animation: rotate 10s linear infinite reverse; + } + + @keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + .circle-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 2; + /* Above wave */ + pointer-events: none; + } + + .water-percentage { + font-family: var(--font-heading); + font-size: 3rem; + font-weight: 700; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); + } + + .water-label { + font-size: 0.8rem; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.7); + margin-top: -5px; + } + + .stats-row { + font-family: var(--font-heading); + font-size: 1.2rem; + color: var(--primary-cyan); + } + + /* Controls */ + .controls-area { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--spacing-md); + align-items: center; + } + + .bottle-selector { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.9rem; + color: var(--text-muted); + } + + .bottle-selector input { + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + padding: 10px; + border-radius: 12px; + font-family: var(--font-heading); + width: 80px; + text-align: center; + font-size: 1.1rem; + outline: none; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.1); + } + + .bottle-selector input:focus { + box-shadow: 0 0 15px rgba(0, 243, 255, 0.3); + } + + .action-buttons { + display: flex; + align-items: center; + gap: 20px; + margin-top: 10px; + } + + .icon-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: var(--text-muted); + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .icon-btn.secondary { + border-color: rgba(255, 50, 50, 0.3); + color: #ff4d4d; + } + + .icon-btn:active { + transform: scale(0.9); + } + + .glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + font-size: 1.1rem; + font-weight: 700; + padding: 0 40px; + height: 60px; + border-radius: 30px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 20px rgba(0, 243, 255, 0.1); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: uppercase; + letter-spacing: 2px; + overflow: hidden; + } + + .glow-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(0, 243, 255, 0.4), transparent); + transition: 0.5s; + } + + .glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(0, 243, 255, 0.8); + } + + .glow-btn:hover::before { + left: 100%; + } + + .glow-btn:active { + transform: scale(0.98); + } + + .glow-btn svg { + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8)); + } + + /* --- Streak Module Styles --- */ + .streak-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); + text-align: center; + } + + .section-title { + font-size: 1.2rem; + color: var(--secondary-purple); + letter-spacing: 4px; + margin-bottom: 20px; + } + + .streak-counter { + background: radial-gradient(circle, rgba(188, 19, 254, 0.1) 0%, transparent 70%); + padding: 40px; + border-radius: 50%; + margin-bottom: 20px; + } + + .streak-days { + font-family: var(--font-heading); + font-size: 6rem; + color: white; + line-height: 1; + text-shadow: 0 0 20px rgba(188, 19, 254, 0.6); + } + + .streak-label { + font-size: 1.5rem; + color: var(--text-muted); + letter-spacing: 5px; + } + + .streak-detail { + font-size: 1rem; + color: var(--primary-cyan); + margin-top: 10px; + } + + .quote-card { + background: rgba(255, 255, 255, 0.05); + border-left: 3px solid var(--primary-cyan); + padding: 20px; + font-style: italic; + color: #dedede; + margin: 20px 0; + line-height: 1.6; + max-width: 300px; + } + + .danger-btn { + position: relative; + background: rgba(255, 50, 50, 0.1); + border: 1px solid #ff4d4d; + color: #ff4d4d; + font-family: var(--font-heading); + padding: 30px 20px 15px; + /* Increased top padding for camera cutout */ + border-radius: 30px; + cursor: pointer; + font-size: 1rem; + letter-spacing: 2px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 30px; + box-shadow: 0 0 10px rgba(255, 50, 50, 0.2), inset 0 0 20px rgba(255, 50, 50, 0.1); + text-transform: uppercase; + font-weight: 700; + } + + .danger-btn:hover { + background: rgba(255, 50, 50, 0.2); + box-shadow: 0 0 30px rgba(255, 50, 50, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(255, 50, 50, 0.8); + transform: translateY(-2px); + } + + .danger-btn:active { + transform: scale(0.95); + } + + /* --- Connect Button (Neon Cyan Variant) --- */ + .connect-glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + padding: 8px 24px; + border-radius: 20px; + cursor: pointer; + font-size: 0.9rem; + letter-spacing: 1px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 10px rgba(0, 243, 255, 0.1); + text-transform: uppercase; + font-weight: 700; + } + + .connect-glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 20px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 5px rgba(0, 243, 255, 0.8); + transform: translateY(-1px); + } + + .connect-glow-btn:active { + transform: scale(0.95); + } + + /* --- Concentric Rings --- */ + .rings-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.02) 0%, transparent 70%); + padding: 20px; + border-radius: 50%; + } + + .concentric-svg { + width: 200px; + height: 200px; + transform: rotate(-90deg); + } + + .ring-bg { + fill: none; + stroke: rgba(255, 255, 255, 0.05); + /* Proper track color */ + } + + .ring-progress { + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 1.5s ease-in-out; + } + + .ring-progress.cyan { + stroke: var(--primary-cyan); + filter: drop-shadow(0 0 5px var(--primary-cyan)); + } + + .ring-progress.purple { + stroke: var(--secondary-purple); + filter: drop-shadow(0 0 5px var(--secondary-purple)); + } + + .rings-legend { + display: flex; + gap: 20px; + margin-top: 15px; + } + + .legend-item { + font-size: 0.8rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 8px; + letter-spacing: 1px; + } + + .dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + + .dot.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px var(--primary-cyan); + } + + .dot.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px var(--secondary-purple); + } + + /* --- Combined Stat Card & Charts --- */ + .stat-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + } + + .stat-label { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 5px; + display: block; + } + + .stat-value { + font-family: var(--font-heading); + font-size: 2rem; + color: #fff; + line-height: 1; + } + + .stat-sub { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + margin-top: 5px; + } + + .icon-box { + width: 40px; + height: 40px; + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + } + + .cyan-box { + background: rgba(0, 243, 255, 0.1); + color: var(--primary-cyan); + } + + .purple-box { + background: rgba(188, 19, 254, 0.1); + color: var(--secondary-purple); + } + + .chart-divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 20px 0; + } + + /* Re-using chart styles but refining for the box */ + .chart-container.small { + height: 100px; + margin-top: 0; + border-bottom: none; + padding-bottom: 0; + display: flex; + justify-content: space-between; + align-items: flex-end; + } + + .chart-column { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + + .chart-bar { + width: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + transition: height 1s ease-out; + } + + .chart-bar.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px rgba(0, 243, 255, 0.3); + } + + .chart-bar.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px rgba(188, 19, 254, 0.3); + } + + .chart-day { + color: var(--text-muted); + } + + /* --- Calendar Module Styles --- */ + .calendar-wrapper { + background: rgba(255, 255, 255, 0.03); + border-radius: 20px; + padding: 20px; + width: 100%; + max-width: 350px; + margin-top: 20px; + border: 1px solid var(--glass-border); + } + + .calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + font-family: var(--font-heading); + color: var(--text-main); + } + + .cal-nav-btn { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.2rem; + cursor: pointer; + padding: 5px 10px; + transition: color 0.3s; + } + + .cal-nav-btn:hover { + color: var(--primary-cyan); + } + + .calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + text-align: center; + } + + .cal-day-label { + color: var(--text-muted); + font-size: 0.7rem; + margin-bottom: 5px; + font-weight: 600; + } + + .cal-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + color: rgba(255, 255, 255, 0.3); + font-size: 0.9rem; + position: relative; + font-family: var(--font-body); + } + + .cal-day.active { + background: rgba(188, 19, 254, 0.2); + color: #fff; + box-shadow: 0 0 10px rgba(188, 19, 254, 0.2); + border: 1px solid rgba(188, 19, 254, 0.4); + } + + .cal-day.start-date { + background: rgba(0, 243, 255, 0.2); + border-color: var(--primary-cyan); + box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); + } + + .cal-day.today::after { + content: ''; + position: absolute; + bottom: 4px; + width: 4px; + height: 4px; + background: #fff; + border-radius: 50%; + } + + /* --- Modal Styles --- */ + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease; + } + + .modal-content { + width: 90%; + max-width: 400px; + background: rgba(10, 10, 20, 0.95); + /* Darker for readability */ + border: 1px solid var(--primary-cyan); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.2); + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid var(--glass-border); + padding-bottom: 10px; + } + + .icon-btn-small { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; + } + + .setting-group { + margin-bottom: 20px; + } + + .setting-group label { + display: block; + color: var(--text-muted); + margin-bottom: 8px; + font-size: 0.9rem; + letter-spacing: 1px; + } + + .glow-input { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: #fff; + padding: 15px; + border-radius: 12px; + font-size: 1.1rem; + font-family: var(--font-heading); + outline: none; + transition: all 0.3s; + } + + .glow-input:focus { + border-color: var(--primary-cyan); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); + } + + .full-width { + width: 100%; + justify-content: center; + margin-top: 10px; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + /* --- Fitness Schedule Styles --- */ + .workout-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 20px; + width: 100%; + margin-bottom: 20px; + position: relative; + overflow: hidden; + } + + .glow-card { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.1); + border-color: rgba(0, 243, 255, 0.3); + } + + .card-header { + margin-bottom: 20px; + } + + .day-badge { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + display: block; + margin-bottom: 5px; + } + + .workout-type { + font-family: var(--font-heading); + font-size: 1.8rem; + line-height: 1.2; + text-transform: uppercase; + } + + .exercise-list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .exercise-item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 10px; + } + + .exercise-item:last-child { + border-bottom: none; + } + + .ex-name { + font-size: 1rem; + color: #e0e0e0; + } + + .ex-meta { + font-family: var(--font-heading); + color: var(--primary-cyan); + font-size: 0.9rem; + text-align: right; + } + + .ex-note { + font-family: var(--font-body); + font-size: 0.8rem; + color: var(--text-muted); + font-weight: normal; + } + + .subsection-title { + font-size: 1rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 15px; + margin-top: 10px; + } + + .schedule-list { + width: 100%; + } + + .schedule-row { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + font-size: 0.9rem; + } + + .sch-day { + color: var(--text-muted); + text-transform: uppercase; + + .cal-day.today::after { + content: ''; + position: absolute; + bottom: 4px; + width: 4px; + height: 4px; + background: #fff; + border-radius: 50%; + } + + /* --- Modal Styles --- */ + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease; + } + + .modal-content { + width: 90%; + max-width: 400px; + background: rgba(10, 10, 20, 0.95); + /* Darker for readability */ + border: 1px solid var(--primary-cyan); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.2); + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid var(--glass-border); + padding-bottom: 10px; + } + + .icon-btn-small { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; + } + + .setting-group { + margin-bottom: 20px; + } + + .setting-group label { + display: block; + color: var(--text-muted); + margin-bottom: 8px; + font-size: 0.9rem; + letter-spacing: 1px; + } + + .glow-input { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: #fff; + padding: 15px; + border-radius: 12px; + font-size: 1.1rem; + font-family: var(--font-heading); + outline: none; + transition: all 0.3s; + } + + .glow-input:focus { + border-color: var(--primary-cyan); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); + } + + .full-width { + width: 100%; + justify-content: center; + margin-top: 10px; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + /* --- Fitness Schedule Styles --- */ + .workout-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 20px; + width: 100%; + margin-bottom: 20px; + position: relative; + overflow: hidden; + } + + .glow-card { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.1); + border-color: rgba(0, 243, 255, 0.3); + } + + .card-header { + margin-bottom: 20px; + } + + .day-badge { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + display: block; + margin-bottom: 5px; + } + + .workout-type { + font-family: var(--font-heading); + font-size: 1.8rem; + line-height: 1.2; + text-transform: uppercase; + } + + .exercise-list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .exercise-item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 10px; + } + + .exercise-item:last-child { + border-bottom: none; + } + + .ex-name { + font-size: 1rem; + color: #e0e0e0; + } + + .ex-meta { + font-family: var(--font-heading); + color: var(--primary-cyan); + font-size: 0.9rem; + text-align: right; + } + + .ex-note { + font-family: var(--font-body); + font-size: 0.8rem; + color: var(--text-muted); + font-weight: normal; + } + + .subsection-title { + font-size: 1rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 15px; + margin-top: 10px; + } + + .schedule-list { + width: 100%; + } + + .schedule-row { + display: flex; + justify-content: space-between; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + } + + .stat-item.cyan .stat-number { + color: var(--primary-cyan); + text-shadow: 0 0 10px rgba(0, 243, 255, 0.4); + } + + .stat-item.purple .stat-number { + color: var(--secondary-purple); + text-shadow: 0 0 10px rgba(188, 19, 254, 0.4); + } + + .stat-label-small { + font-size: 0.75rem; + color: var(--text-muted); + letter-spacing: 2px; + display: flex; + align-items: center; + gap: 6px; + } + + .stat-number { + font-family: var(--font-heading); + font-size: 1.8rem; + font-weight: 700; + } + + /* --- Single File Animations --- */ + @keyframes wave { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-10px); + } + } + + .wave-animation { + animation: wave 3s ease-in-out infinite; + } + + @keyframes float { + + 0%, + 100% { + transform: translateY(0px); + } + + 50% { + transform: translateY(-5px); + } + } + + .float-animation { + animation: float 2s ease-in-out infinite; + } + + .water-container { + transition: height 0.5s ease-out; + } + + /* Custom scrollbar for notes */ + textarea::-webkit-scrollbar { + width: 4px; + } + + textarea::-webkit-scrollbar-track { + background: transparent; + } + + textarea::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 2px; + } + } + + /* Notes Module Full Page */ + .notes-container { + + .note-input-area { + margin-bottom: 20px; + } + + .notes-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + } + + .sticky-note { + background: #FF9; + /* Classic yellow */ + color: #333; + padding: 15px; + border-radius: 2px; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + min-height: 120px; + display: flex; + flex-direction: column; + justify-content: space-between; + font-family: 'Courier New', monospace; + transform: rotate(-1deg); + transition: transform 0.2s; + } + + .sticky-note:nth-child(even) { + background: #CCF; + /* Blue tint */ + transform: rotate(1deg); + } + + .sticky-note:nth-child(3n) { + background: #CFC; + /* Green tint */ + } + + .sticky-note:hover { + transform: scale(1.05) rotate(0deg); + z-index: 5; + } + + .note-content { + font-size: 0.9rem; + line-height: 1.4; + } + + .note-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; + border-top: 1px solid rgba(0, 0, 0, 0.1); + padding-top: 5px; + } + + .note-date { + font-size: 0.65rem; + color: #666; + } + + .delete-note-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + opacity: 0.5; + transition: opacity 0.2s; + } + + .delete-note-btn:hover { + opacity: 1; + color: red; + } \ No newline at end of file diff --git a/Hydroflux/app/src/main/assets/index.html b/Hydroflux/app/src/main/assets/index.html index 98282f2..4af37f8 100644 --- a/Hydroflux/app/src/main/assets/index.html +++ b/Hydroflux/app/src/main/assets/index.html @@ -6,7 +6,7 @@ HydroFit - + @@ -23,6 +23,9 @@ + + + - -
- -
-

HYDROFIT

-
-
- -
- -
- -
Loading Core...
-
- - - - - - - - -
- - - - - + +
+
+ + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/assets/js/app.js b/Hydroflux/app/src/main/assets/js/app.js index 816dccf..b2a1c6f 100644 --- a/Hydroflux/app/src/main/assets/js/app.js +++ b/Hydroflux/app/src/main/assets/js/app.js @@ -1,43 +1,72 @@ -console.log('HydroFlux Initialized'); +import { Dashboard } from './modules/dashboard.js'; +import { WaterView } from './modules/views/WaterView.js'; +import { FitnessView } from './modules/views/FitnessView.js'; +import { SleepView } from './modules/views/SleepView.js'; +import { GoalsView } from './modules/views/GoalsView.js'; +import { NotesView } from './modules/views/NotesView.js'; -// Simple Navigation Logic +class App { + constructor() { + this.appContainer = document.getElementById('app'); + this.currentView = null; + this.routes = { + 'dashboard': Dashboard, + 'water-detail': WaterView, + 'fitness-detail': FitnessView, + 'sleep-detail': SleepView, + 'goals-detail': GoalsView, + 'notes-detail': NotesView + }; -// Simple Navigation Logic - REPLACED BY NEW BLOCK BELOW - -// Initialize Modules -import { WaterTracker } from './modules/water.js'; -import { StreakTracker } from './modules/streak.js'; -import { FitnessDashboard } from './modules/fitness.js'; -import { StatsDashboard } from './modules/stats.js'; -import { GoalsTracker } from './modules/goals.js'; - -const waterTracker = new WaterTracker('water-section'); -const streakTracker = new StreakTracker('streak-section'); -const fitnessDashboard = new FitnessDashboard('fitness-section'); -const statsDashboard = new StatsDashboard('stats-section'); -const goalsTracker = new GoalsTracker('goals-section'); - -// Navigation Logic with Auto-Refresh -document.querySelectorAll('.nav-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - // Toggle Active State - document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); - e.currentTarget.classList.add('active'); - - const view = e.currentTarget.dataset.view; - - // Hide all sections - document.querySelectorAll('section').forEach(el => el.style.display = 'none'); - - // Show target section - const target = document.getElementById(`${view}-section`); - if (target) { - target.style.display = 'block'; - - // Auto-Refresh Stats when viewing - if (view === 'stats') { - statsDashboard.update(); + // Handle browser back button + window.addEventListener('popstate', (e) => { + if (e.state && e.state.view) { + this.render(e.state.view, e.state.params); + } else { + this.render('dashboard'); } + }); + + this.init(); + } + + init() { + console.log('HydroFlux Router Initialized'); + this.navigateTo('dashboard'); + } + + navigateTo(viewName, params = {}) { + history.pushState({ view: viewName, params }, '', `#${viewName}`); + this.render(viewName, params); + } + + back() { + // history.back() can be tricky if state isn't perfect, safer to just go dashboard for now + this.navigateTo('dashboard'); + } + + async render(viewName, params) { + // Cleanup current view if needed (optional destroy method) + if (this.currentView && typeof this.currentView.destroy === 'function') { + this.currentView.destroy(); } - }); + + this.appContainer.innerHTML = ''; // Clear container + + const ViewClass = this.routes[viewName] || this.routes['dashboard']; + + // Dynamic Import for Views (if we wanted lazy loading, but consistent imports are easier for now) + // For now, we instantiate directly. + + this.currentView = new ViewClass('app', this, params); + // Note: Dashboard expects (containerId, app). We'll keep that signature. + } +} + +// Start App +const app = new App(); + +// Global health bridge listener +window.addEventListener('health-update', (e) => { + console.log('Health Update Received:', e.detail); }); diff --git a/Hydroflux/app/src/main/assets/js/modules/dashboard.js b/Hydroflux/app/src/main/assets/js/modules/dashboard.js new file mode 100644 index 0000000..8e0cb5b --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/dashboard.js @@ -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 = ` +
+ +
+
+ + +
+
+ +
+
+
+ JD +
+
+ Level 14 +
+ + +
+

Hydro Flux

+
+
+
+ + +
+ + + + + + + 14 Days +
+
+
+ + +
+ +
+ + + + +
+ + + + + +
+ ${parseFloat(waterData.current).toFixed(1)}L / ${parseFloat(waterData.goal).toFixed(1)}L +
+
+ + +
+ +
+

Steps Tracker

+
+
+ + + + +
+ ${stepsData.toLocaleString()} + steps +
+
+
+
+ + +
+

Strava Integration

+
+
+ S +
+ 5.2km Run + Last Sync: 2h ago +
+
+
+ + +
+

Sleep Tracker

+
+ + + +
+
+ ${sleepHours}h ${sleepMins}m + Light Sleep +
+
+ + +
+ +
+

Goals

+
+ +
+
+ + +
+

Notes

+ +
+
+
+ `; + + 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 => ` + + `).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 += ' View All →'; + 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); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/notes.js b/Hydroflux/app/src/main/assets/js/modules/notes.js new file mode 100644 index 0000000..b3eef3f --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/notes.js @@ -0,0 +1,89 @@ +export class NotesTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.STORAGE_KEY = 'hydroflux_notes'; + this.notes = []; + this.loadState(); + this.render(); + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + this.notes = JSON.parse(saved); + } else { + this.notes = [ + { id: 1, text: "Feeling hydrated today. Remember to add those gym sessions!", date: new Date().toISOString() } + ]; + this.saveState(); + } + } + + saveState() { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.notes)); + } + + addNote(text) { + if (!text.trim()) return; + this.notes.unshift({ + id: Date.now(), + text: text, + date: new Date().toISOString() + }); + this.saveState(); + this.render(); + } + + deleteNote(id) { + if (confirm("Delete this note?")) { + this.notes = this.notes.filter(n => n.id !== id); + this.saveState(); + this.render(); + } + } + + render() { + if (!this.container) return; + + this.container.innerHTML = ` +
+

NOTES

+ +
+ + +
+ +
+ ${this.notes.map(note => ` +
+

${note.text}

+ +
+ `).join('')} +
+
+ `; + + this.attachEvents(); + } + + attachEvents() { + const btn = this.container.querySelector('#add-note-btn'); + const input = this.container.querySelector('#new-note-text'); + + btn.addEventListener('click', () => { + this.addNote(input.value); + input.value = ''; + }); + + this.container.querySelectorAll('.delete-note-btn').forEach(b => { + b.addEventListener('click', (e) => { + this.deleteNote(parseInt(e.currentTarget.dataset.id)); + }); + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/views/FitnessView.js b/Hydroflux/app/src/main/assets/js/modules/views/FitnessView.js new file mode 100644 index 0000000..d8987d3 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/views/FitnessView.js @@ -0,0 +1,84 @@ +export class FitnessView { + constructor(containerId, app) { + this.container = document.getElementById(containerId); + this.app = app; + this.render(); + this.attachEvents(); + } + + render() { + // Mock Data + const steps = 8432; + const goal = 10000; + const pct = Math.min((steps / goal) * 100, 100); + const radius = 80; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (pct / 100) * circumference; + + this.container.innerHTML = ` +
+ +
+ + Fitness Activity +
+
+ + +
+
+ + + + +
+ ${steps.toLocaleString()} + steps today +
+
+
+ + +
+
+ Calories + 420 + kcal +
+
+ Distance + 5.2 + km +
+
+ + +
+
+ +
+ + + +
+
+
S
+
+
Morning Run
+
Synced via Strava
+
+
+
+
+ `; + } + + attachEvents() { + this.container.querySelector('#backBtn').addEventListener('click', () => { + this.app.back(); + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/views/GoalsView.js b/Hydroflux/app/src/main/assets/js/modules/views/GoalsView.js new file mode 100644 index 0000000..495ae42 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/views/GoalsView.js @@ -0,0 +1,120 @@ +export class GoalsView { + constructor(containerId, app) { + this.container = document.getElementById(containerId); + this.app = app; + this.render(); + this.attachEvents(); + } + + render() { + const goalsData = JSON.parse(localStorage.getItem('hydroflux_goals') || '[]'); + + this.container.innerHTML = ` +
+ +
+ + Your Goals + +
+ + +
+ ${goalsData.map(g => ` +
+ +
+
${g.text}
+
Daily Goal
+
+ +
+ `).join('')} + + ${goalsData.length === 0 ? ` +
+ No goals set. Tap + to add one! +
+ ` : ''} +
+ + + +
+ `; + } + + attachEvents() { + this.container.querySelector('#backBtn').addEventListener('click', () => this.app.back()); + + // Toggle Done + this.container.querySelectorAll('.toggle-goal').forEach(btn => { + btn.addEventListener('click', () => { + const id = btn.dataset.id; + const goals = JSON.parse(localStorage.getItem('hydroflux_goals') || '[]'); + const goal = goals.find(g => g.id === id); + if (goal) { + goal.completed = !goal.completed; + localStorage.setItem('hydroflux_goals', JSON.stringify(goals)); + this.render(); + this.attachEvents(); + } + }); + }); + + // Delete + this.container.querySelectorAll('.delete-goal').forEach(btn => { + btn.addEventListener('click', () => { + const id = btn.dataset.id; + let goals = JSON.parse(localStorage.getItem('hydroflux_goals') || '[]'); + goals = goals.filter(g => g.id !== id); + localStorage.setItem('hydroflux_goals', JSON.stringify(goals)); + this.render(); + this.attachEvents(); + }); + }); + + + // Add Panel Logic + const panel = this.container.querySelector('#newGoalPanel'); + this.container.querySelector('#addGoalProto').addEventListener('click', () => { + panel.classList.remove('hidden'); + this.container.querySelector('#newGoalInput').focus(); + }); + + this.container.querySelector('#cancelAdd').addEventListener('click', () => { + panel.classList.add('hidden'); + }); + + this.container.querySelector('#saveGoal').addEventListener('click', () => { + const input = this.container.querySelector('#newGoalInput'); + const text = input.value.trim(); + if (text) { + const goals = JSON.parse(localStorage.getItem('hydroflux_goals') || '[]'); + goals.push({ + id: Date.now().toString(), + text: text, + completed: false + }); + localStorage.setItem('hydroflux_goals', JSON.stringify(goals)); + this.render(); + this.attachEvents(); + } + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/views/NotesView.js b/Hydroflux/app/src/main/assets/js/modules/views/NotesView.js new file mode 100644 index 0000000..d99d644 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/views/NotesView.js @@ -0,0 +1,63 @@ +export class NotesView { + constructor(containerId, app) { + this.container = document.getElementById(containerId); + this.app = app; + this.render(); + this.attachEvents(); + } + + render() { + const currentNote = localStorage.getItem('hydroflux_note_content') || ''; + + this.container.innerHTML = ` +
+ +
+ + Notes & Thoughts + +
+ + +
+ + +
Auto-saved
+
+ + +
+
+ `; + + // Focus textarea automatically + setTimeout(() => { + const el = this.container.querySelector('#fullNoteInput'); + if (el) { + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); + } + }, 300); + } + + attachEvents() { + this.container.querySelector('#backBtn').addEventListener('click', () => this.app.back()); + + const textarea = this.container.querySelector('#fullNoteInput'); + const status = this.container.querySelector('#saveStatus'); + + textarea.addEventListener('input', (e) => { + status.textContent = 'Saving...'; + localStorage.setItem('hydroflux_note_content', e.target.value); + setTimeout(() => { + status.textContent = 'Auto-saved'; + }, 800); + }); + + this.container.querySelector('#saveNoteBtn').addEventListener('click', () => this.app.back()); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/views/SleepView.js b/Hydroflux/app/src/main/assets/js/modules/views/SleepView.js new file mode 100644 index 0000000..e286e26 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/views/SleepView.js @@ -0,0 +1,98 @@ +export class SleepView { + constructor(containerId, app) { + this.container = document.getElementById(containerId); + this.app = app; + this.render(); + this.attachEvents(); + } + + render() { + // Mock Sleep Data + const sleepHours = 7; + const sleepMins = 20; + + this.container.innerHTML = ` +
+ +
+ + Sleep Analysis +
+
+ + +
+
+ ${sleepHours} + h + ${sleepMins} + m +
+
Total Sleep Duration
+
+ + +
+

Sleep Stages

+ +
+ +
+
+ REM + 1h 45m +
+
+
+
+
+ + +
+
+ Deep Sleep + 2h 10m +
+
+
+
+
+ + +
+
+ Light Sleep + 3h 25m +
+
+
+
+
+
+
+ + +
+

Weekly Trend

+
+ ${[6.5, 7.2, 5.8, 8.0, 7.5, 6.2, 7.3].map((val, i) => ` +
+
+ ${['M', 'T', 'W', 'T', 'F', 'S', 'S'][i]} +
+ `).join('')} +
+
+
+ `; + } + + attachEvents() { + this.container.querySelector('#backBtn').addEventListener('click', () => { + this.app.back(); + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/views/WaterView.js b/Hydroflux/app/src/main/assets/js/modules/views/WaterView.js new file mode 100644 index 0000000..59436e1 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/views/WaterView.js @@ -0,0 +1,196 @@ +export class WaterView { + constructor(containerId, app) { + this.container = document.getElementById(containerId); + this.app = app; + this.history = JSON.parse(localStorage.getItem('hydroflux_water_history') || '[]'); + this.render(); + this.attachEvents(); + } + + render() { + const data = JSON.parse(localStorage.getItem('hydroflux_data') || '{"current":1.2,"goal":3.0}'); + const drinkSize = parseInt(localStorage.getItem('hydroflux_drink_size') || '250'); + const percentage = Math.min((data.current / data.goal) * 100, 100); + + this.container.innerHTML = ` +
+ +
+ + Hydration +
+
+ + +
+ + +
+
+ +
+ + + +
+ + +
+
${data.current.toFixed(1)}L
+
of ${data.goal.toFixed(1)}L Goal
+
+
+
+ + +
+

Today's History

+
+ ${this.history.length === 0 ? + '
No drinks logged yet today.
' : + this.history.slice().reverse().map(entry => ` +
+
+
+ +
+
+
Water
+
${new Date(entry.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+ +${entry.amount}ml +
+ `).join('')} +
+
+
+ + +
+
+ + + + + +
+ Size +
+ ${drinkSize} +
+ +
+
+
+
+
+ + + +
+ + `; + } + + addHistory(amount) { + this.history.push({ + timestamp: Date.now(), + amount: amount + }); + localStorage.setItem('hydroflux_water_history', JSON.stringify(this.history)); + } + + attachEvents() { + this.container.querySelector('#backBtn').addEventListener('click', () => { + this.app.back(); + }); + + const updateWater = (amount) => { + const data = JSON.parse(localStorage.getItem('hydroflux_data') || '{"current":1.2,"goal":3.0}'); + data.current = Math.min(data.current + (amount / 1000), data.goal); // amount in ml to L + localStorage.setItem('hydroflux_data', JSON.stringify(data)); + this.addHistory(amount); + this.render(); + this.attachEvents(); + }; + + this.container.querySelector('#addWater').addEventListener('click', () => { + const drinkSize = parseInt(localStorage.getItem('hydroflux_drink_size') || '250'); + updateWater(drinkSize); + }); + + this.container.querySelector('#removeWater').addEventListener('click', () => { + // Removing doesn't add to history, usually just undoes + const data = JSON.parse(localStorage.getItem('hydroflux_data') || '{"current":1.2,"goal":3.0}'); + const drinkSize = parseInt(localStorage.getItem('hydroflux_drink_size') || '250'); + data.current = Math.max(data.current - (drinkSize / 1000), 0); + localStorage.setItem('hydroflux_data', JSON.stringify(data)); + + // Optionally remove last history entry if it matches? + // For now just keep it simple + this.render(); + this.attachEvents(); + }); + + // Modal Logic + const modal = this.container.querySelector('#sizeModal'); + const openModal = () => modal.classList.remove('hidden'); + const closeModal = () => modal.classList.add('hidden'); + + this.container.querySelector('#drinkSizeBtn').addEventListener('click', openModal); + this.container.querySelector('#closeModal').addEventListener('click', closeModal); + this.container.querySelector('#closeModalMask').addEventListener('click', closeModal); + + this.container.querySelectorAll('.size-option').forEach(btn => { + btn.addEventListener('click', () => { + localStorage.setItem('hydroflux_drink_size', btn.dataset.size); + closeModal(); + this.render(); + this.attachEvents(); + }); + }); + + // Custom Input Logic + this.container.querySelector('#applyCustom').addEventListener('click', () => { + const val = this.container.querySelector('#customAmountInput').value; + if (val && val > 0) { + localStorage.setItem('hydroflux_drink_size', val); + closeModal(); + this.render(); + this.attachEvents(); + } + }); + } +}