HydroFlux 0.0.2
This commit is contained in:
6
Hydroflux/.idea/vcs.xml
generated
Normal file
6
Hydroflux/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
536
Hydroflux/REPLICATION_GUIDE.md
Normal file
536
Hydroflux/REPLICATION_GUIDE.md
Normal file
@@ -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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-white rounded-[3rem] shadow-2xl p-6 relative overflow-hidden">
|
||||
{/* Subtle background decoration */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-200/20 rounded-full blur-3xl -z-10" />
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 bg-purple-200/20 rounded-full blur-3xl -z-10" />
|
||||
|
||||
<Header
|
||||
level={14}
|
||||
streakDays={14}
|
||||
currentDate={currentDate}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
|
||||
<WaterTracker
|
||||
currentAmount={waterAmount}
|
||||
targetAmount={3.0}
|
||||
onAddWater={handleAddWater}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<StepsTracker steps={8432} goal={10000} />
|
||||
<StravaIntegration distance={5.2} lastSync="2h ago" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<SleepTracker
|
||||
hours={7}
|
||||
minutes={20}
|
||||
sleepType="Light Sleep"
|
||||
data={sleepData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Goals goals={goals} onToggleGoal={handleToggleGoal} />
|
||||
<Notes note={note} onNoteChange={setNote} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col items-center gap-2 mb-6">
|
||||
<div className="flex items-center justify-between w-full px-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="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 className="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center text-sm font-semibold text-gray-700">
|
||||
JD
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-blue-600 font-medium">Level {level}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-1">Hydro Flux</h1>
|
||||
<div className="text-sm text-gray-600">{currentDate}</div>
|
||||
<div className="text-sm text-gray-500">{currentTime}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Droplet className="w-5 h-5 fill-blue-500 text-blue-500" />
|
||||
<Droplet className="w-5 h-5 fill-blue-400 text-blue-400" />
|
||||
<span className="text-sm font-semibold text-gray-700 ml-1">{streakDays} Days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div className="relative h-64 bg-gradient-to-b from-blue-50 to-blue-100 rounded-3xl overflow-hidden shadow-lg mb-6">
|
||||
{/* Water wave */}
|
||||
<motion.div
|
||||
className="absolute bottom-0 left-0 right-0"
|
||||
style={{
|
||||
height: `${percentage}%`,
|
||||
background: 'linear-gradient(180deg, rgba(96, 165, 250, 0.8) 0%, rgba(59, 130, 246, 0.9) 100%)',
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
{/* Wave effect */}
|
||||
<svg
|
||||
className="absolute top-0 left-0 w-full"
|
||||
viewBox="0 0 1200 120"
|
||||
preserveAspectRatio="none"
|
||||
style={{ height: '60px', transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<motion.path
|
||||
d="M0,50 Q300,10 600,50 T1200,50 L1200,120 L0,120 Z"
|
||||
fill="rgba(96, 165, 250, 0.5)"
|
||||
animate={{
|
||||
d: [
|
||||
"M0,50 Q300,10 600,50 T1200,50 L1200,120 L0,120 Z",
|
||||
"M0,50 Q300,90 600,50 T1200,50 L1200,120 L0,120 Z",
|
||||
"M0,50 Q300,10 600,50 T1200,50 L1200,120 L0,120 Z",
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
|
||||
{/* Add button */}
|
||||
<button
|
||||
onClick={onAddWater}
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-16 h-16 bg-blue-400/50 hover:bg-blue-400/70 backdrop-blur-sm rounded-full flex items-center justify-center shadow-xl transition-all hover:scale-110 z-10"
|
||||
>
|
||||
<Plus className="w-8 h-8 text-white" />
|
||||
</button>
|
||||
|
||||
{/* Amount display */}
|
||||
<div className="absolute bottom-6 left-0 right-0 text-center z-10">
|
||||
<span className="text-2xl font-bold text-gray-700">
|
||||
{currentAmount.toFixed(1)}L / {targetAmount.toFixed(1)}L
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div className="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4">Steps Tracker</h3>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="relative w-32 h-32">
|
||||
<svg className="transform -rotate-90 w-32 h-32">
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r="45"
|
||||
stroke="#E5E7EB"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r="45"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-500"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold text-gray-800">{steps.toLocaleString()}</span>
|
||||
<span className="text-xs text-gray-500">steps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. /src/app/components/StravaIntegration.tsx
|
||||
|
||||
```tsx
|
||||
interface StravaIntegrationProps {
|
||||
distance: number;
|
||||
lastSync: string;
|
||||
}
|
||||
|
||||
export function StravaIntegration({ distance, lastSync }: StravaIntegrationProps) {
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-orange-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4">Strava Integration</h3>
|
||||
<div className="flex flex-col items-center justify-center py-4">
|
||||
<div className="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 className="text-3xl font-bold text-white transform -rotate-12">S</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-gray-800">{distance}km Run</span>
|
||||
<span className="text-xs text-gray-500 mt-1">Last Sync: {lastSync}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div className="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4">Sleep Tracker</h3>
|
||||
<div className="h-24 mb-3">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data}>
|
||||
<YAxis hide domain={[0, 100]} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-bold text-gray-800">
|
||||
{hours}h {minutes}m
|
||||
</span>
|
||||
<span className="text-sm text-blue-600 font-medium">{sleepType}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div className="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4">Goals</h3>
|
||||
<div className="space-y-3">
|
||||
{goals.map((goal) => (
|
||||
<button
|
||||
key={goal.id}
|
||||
onClick={() => onToggleGoal(goal.id)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all ${
|
||||
goal.completed
|
||||
? 'bg-blue-500 text-white shadow-lg'
|
||||
: 'bg-blue-100 text-gray-700 hover:bg-blue-200'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full flex items-center justify-center ${
|
||||
goal.completed ? 'bg-white/30' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
{goal.completed && <Check className="w-4 h-4 text-blue-500" />}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{goal.text}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8. /src/app/components/Notes.tsx
|
||||
|
||||
```tsx
|
||||
interface NotesProps {
|
||||
note: string;
|
||||
onNoteChange: (note: string) => void;
|
||||
}
|
||||
|
||||
export function Notes({ note, onNoteChange }: NotesProps) {
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-blue-50 to-white rounded-2xl p-6 shadow-md">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4">Notes</h3>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => onNoteChange(e.target.value)}
|
||||
placeholder="Add your notes here..."
|
||||
className="w-full h-24 bg-white/50 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-400 border-none outline-none resize-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features & Interactions
|
||||
|
||||
1. **Water Tracker**
|
||||
- Animated wave visualization using SVG and Motion
|
||||
- Click + button to add 0.25L (max 3.0L)
|
||||
- Displays current/target amount
|
||||
|
||||
2. **Steps Tracker**
|
||||
- Circular progress indicator using SVG circles
|
||||
- Shows 8,432 / 10,000 steps
|
||||
- Animated stroke-dashoffset for progress
|
||||
|
||||
3. **Strava Integration**
|
||||
- Orange gradient background
|
||||
- Rotated "S" logo
|
||||
- Displays distance and last sync time
|
||||
|
||||
4. **Sleep Tracker**
|
||||
- Recharts line chart visualization
|
||||
- Shows hours and minutes
|
||||
- Sleep type label (Light Sleep)
|
||||
|
||||
5. **Goals**
|
||||
- Three clickable goal items
|
||||
- Toggle completed state
|
||||
- Visual feedback with color change
|
||||
|
||||
6. **Notes**
|
||||
- Textarea for user input
|
||||
- Placeholder text
|
||||
- Auto-updating state
|
||||
|
||||
7. **Header**
|
||||
- User avatar with initials "JD"
|
||||
- Level indicator
|
||||
- Streak counter with droplet icons
|
||||
- Live date/time that updates every second
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
- Use Tailwind CSS v4 (no config file needed)
|
||||
- Motion package is imported as `import { motion } from 'motion/react'`
|
||||
- All components are TypeScript with proper interfaces
|
||||
- State management uses React useState hooks
|
||||
- DateTime updates every 1000ms using setInterval
|
||||
- Responsive design with max-width container
|
||||
- All interactive elements have hover states
|
||||
- Glassmorphism effects using backdrop-blur and opacity
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Water tracker + button increases amount
|
||||
- [ ] Water amount doesn't exceed 3.0L
|
||||
- [ ] Goals toggle between completed/incomplete
|
||||
- [ ] Notes textarea updates on input
|
||||
- [ ] Date and time update in real-time
|
||||
- [ ] All animations are smooth
|
||||
- [ ] Mobile responsive (320px - 448px width)
|
||||
- [ ] All icons from lucide-react render correctly
|
||||
- [ ] Sleep chart displays properly with recharts
|
||||
266
Hydroflux/Single_File.html.txt
Normal file
266
Hydroflux/Single_File.html.txt
Normal file
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hydro Flux Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-gray-100 to-gray-200 min-h-screen flex items-center justify-center p-4">
|
||||
|
||||
<div class="w-full max-w-md bg-white rounded-[3rem] shadow-2xl p-6 relative overflow-hidden">
|
||||
<!-- 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" xmlns="http://www.w3.org/2000/svg">
|
||||
<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" xmlns="http://www.w3.org/2000/svg">
|
||||
<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="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: 40%; 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" xmlns="http://www.w3.org/2000/svg">
|
||||
<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">1.2L / 3.0L</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="stroke-dasharray: 282.74; stroke-dashoffset: 56.55; 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">8,432</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">7h 20m</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">
|
||||
<button class="goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all bg-blue-500 text-white shadow-lg" data-completed="true">
|
||||
<div class="w-5 h-5 rounded-full flex items-center justify-center bg-white bg-opacity-30">
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium">Drink 3L of water</span>
|
||||
</button>
|
||||
|
||||
<button class="goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all bg-blue-100 text-gray-700 hover:bg-blue-200" data-completed="false">
|
||||
<div class="w-5 h-5 rounded-full flex items-center justify-center bg-white"></div>
|
||||
<span class="text-sm font-medium">Walk 10K steps</span>
|
||||
</button>
|
||||
|
||||
<button class="goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all bg-blue-100 text-gray-700 hover:bg-blue-200" data-completed="false">
|
||||
<div class="w-5 h-5 rounded-full flex items-center justify-center bg-white"></div>
|
||||
<span class="text-sm font-medium">Sleep 8 hours</span>
|
||||
</button>
|
||||
</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">Feeling hydrated today. Remember to add those gym sessions!</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Water tracking
|
||||
let waterAmount = 1.2;
|
||||
const maxWater = 3.0;
|
||||
const waterIncrement = 0.25;
|
||||
|
||||
function updateWaterDisplay() {
|
||||
const percentage = (waterAmount / maxWater) * 100;
|
||||
document.getElementById('waterFill').style.height = percentage + '%';
|
||||
document.getElementById('waterAmount').textContent = waterAmount.toFixed(1) + 'L / ' + maxWater.toFixed(1) + 'L';
|
||||
}
|
||||
|
||||
document.getElementById('addWaterBtn').addEventListener('click', function() {
|
||||
if (waterAmount < maxWater) {
|
||||
waterAmount = Math.min(waterAmount + waterIncrement, maxWater);
|
||||
updateWaterDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
// DateTime update
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
const dateOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const timeOptions = { hour: 'numeric', minute: '2-digit', hour12: true };
|
||||
|
||||
document.getElementById('currentDate').textContent = now.toLocaleDateString('en-US', dateOptions);
|
||||
document.getElementById('currentTime').textContent = now.toLocaleTimeString('en-US', timeOptions) + ' AEDT';
|
||||
}
|
||||
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
|
||||
// Steps circle calculation
|
||||
const steps = 8432;
|
||||
const goal = 10000;
|
||||
const percentage = Math.min((steps / goal) * 100, 100);
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
||||
document.getElementById('stepsCircle').style.strokeDasharray = circumference;
|
||||
document.getElementById('stepsCircle').style.strokeDashoffset = strokeDashoffset;
|
||||
|
||||
// Sleep chart
|
||||
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(' ');
|
||||
document.getElementById('sleepLine').setAttribute('points', points);
|
||||
|
||||
// Goals toggle
|
||||
document.querySelectorAll('.goal-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const isCompleted = this.getAttribute('data-completed') === 'true';
|
||||
const newState = !isCompleted;
|
||||
|
||||
this.setAttribute('data-completed', newState);
|
||||
|
||||
if (newState) {
|
||||
this.className = 'goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all bg-blue-500 text-white shadow-lg';
|
||||
this.querySelector('div').className = 'w-5 h-5 rounded-full flex items-center justify-center bg-white bg-opacity-30';
|
||||
this.querySelector('div').innerHTML = '<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
|
||||
} else {
|
||||
this.className = 'goal-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all bg-blue-100 text-gray-700 hover:bg-blue-200';
|
||||
this.querySelector('div').className = 'w-5 h-5 rounded-full flex items-center justify-center bg-white';
|
||||
this.querySelector('div').innerHTML = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize
|
||||
updateWaterDisplay();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>HydroFit</title>
|
||||
<meta name="description" content="Futuristic Hydration & Fitness Tracker">
|
||||
<meta name="theme-color" content="#0a0a12">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- PWA Application Settings -->
|
||||
<link rel="manifest" href="manifest.json">
|
||||
@@ -23,6 +23,9 @@
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
<!-- Tailwind CSS (CDN) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- DEBUGGING SCRIPT -->
|
||||
<script>
|
||||
// Production Error Handling
|
||||
@@ -32,104 +35,20 @@
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Main Layout injected here -->
|
||||
<header class="glass-header">
|
||||
<h1 class="glow-text">HYDRO<span class="highlight">FIT</span></h1>
|
||||
<div id="connection-status" class="status-indicator"></div>
|
||||
</header>
|
||||
|
||||
<main id="main-content">
|
||||
<!-- Dynamic Content -->
|
||||
<section id="water-section" class="glass-panel">
|
||||
<!-- Water Tracker injected via JS -->
|
||||
<div class="loader">Loading Core...</div>
|
||||
</section>
|
||||
|
||||
<section id="streak-section" class="glass-panel" style="display: none;">
|
||||
<!-- Streak Tracker injected via JS -->
|
||||
</section>
|
||||
|
||||
<section id="fitness-section" class="glass-panel" style="display: none;">
|
||||
<h2>FITNESS DATA</h2>
|
||||
<div id="fitness-container">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="stats-section" class="glass-panel" style="display: none;">
|
||||
<!-- Stats injected via JS -->
|
||||
</section>
|
||||
|
||||
<section id="goals-section" class="glass-panel" style="display: none;">
|
||||
<!-- Goals injected via JS -->
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Settings Modal (Glassmorphism) -->
|
||||
<div id="settings-modal" class="modal-overlay" style="display: none;">
|
||||
<div class="glass-panel modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>SETTINGS</h2>
|
||||
<button id="close-settings-btn" class="icon-btn-small">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>Daily Water Goal (mL)</label>
|
||||
<input type="number" id="setting-goal-input" class="glow-input">
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>Custom Bottle Size (mL)</label>
|
||||
<input type="number" id="setting-bottle-input" class="glow-input">
|
||||
</div>
|
||||
|
||||
<button id="save-settings-btn" class="glow-btn full-width">SAVE CHANGES</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="glass-nav">
|
||||
<button class="nav-btn active" data-view="water">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-view="streak">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-view="fitness">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-view="stats">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-view="goals">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" id="open-settings-btn">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
<body class="bg-gradient-to-br from-gray-100 to-gray-200 min-h-screen flex items-center justify-center p-4">
|
||||
<div id="app" class="w-full">
|
||||
<!-- Main Dashboard Component Injected Here -->
|
||||
</div>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
|
||||
<!-- Connector for Android -->
|
||||
<script>
|
||||
// Placeholder for Android bridge
|
||||
window.updateHealthData = (steps, sleep) => {
|
||||
// Dispatch event for internal logic
|
||||
window.dispatchEvent(new CustomEvent('health-update', { detail: { steps, sleep } }));
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
89
Hydroflux/app/src/main/assets/js/modules/notes.js
Normal file
89
Hydroflux/app/src/main/assets/js/modules/notes.js
Normal file
@@ -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 = `
|
||||
<div class="notes-container">
|
||||
<h2 class="section-title">NOTES</h2>
|
||||
|
||||
<div class="note-input-area">
|
||||
<textarea id="new-note-text" placeholder="Write a thought..." rows="3" class="glow-input"></textarea>
|
||||
<button id="add-note-btn" class="connect-glow-btn full-width">SAVE NOTE</button>
|
||||
</div>
|
||||
|
||||
<div class="notes-grid">
|
||||
${this.notes.map(note => `
|
||||
<div class="sticky-note">
|
||||
<p class="note-content">${note.text}</p>
|
||||
<div class="note-footer">
|
||||
<span class="note-date">${new Date(note.date).toLocaleDateString()}</span>
|
||||
<button class="delete-note-btn" data-id="${note.id}">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 = `
|
||||
<div class="w-full max-w-md bg-white min-h-screen relative overflow-hidden mx-auto bg-gradient-to-br from-blue-50 to-white flex flex-col p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<button id="backBtn" class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-md">
|
||||
<svg class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<span class="text-lg font-bold text-gray-800">Fitness Activity</span>
|
||||
<div class="w-10"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Ring -->
|
||||
<div class="flex flex-col items-center justify-center mb-8">
|
||||
<div class="relative w-64 h-64">
|
||||
<svg class="transform -rotate-90 w-64 h-64 drop-shadow-xl">
|
||||
<circle cx="128" cy="128" r="80" stroke="#E5E7EB" stroke-width="16" fill="none"></circle>
|
||||
<circle cx="128" cy="128" r="80" stroke="#60A5FA" stroke-width="16" fill="none" stroke-linecap="round"
|
||||
style="stroke-dasharray: ${circumference}; stroke-dashoffset: ${offset}; transition: stroke-dashoffset 1s ease-out;"></circle>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span class="text-4xl font-bold text-gray-800">${steps.toLocaleString()}</span>
|
||||
<span class="text-gray-500 font-medium">steps today</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100 flex flex-col items-center">
|
||||
<span class="text-gray-400 text-xs uppercase font-bold tracking-wider mb-1">Calories</span>
|
||||
<span class="text-2xl font-bold text-orange-500">420</span>
|
||||
<span class="text-xs text-gray-400">kcal</span>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100 flex flex-col items-center">
|
||||
<span class="text-gray-400 text-xs uppercase font-bold tracking-wider mb-1">Distance</span>
|
||||
<span class="text-2xl font-bold text-blue-500">5.2</span>
|
||||
<span class="text-xs text-gray-400">km</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strava Map Placeholder -->
|
||||
<div class="bg-white rounded-3xl p-4 shadow-md flex-1 mb-4 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gray-100 z-0">
|
||||
<!-- CSS Pattern for fake map -->
|
||||
<div style="background-image: radial-gradient(#cbd5e1 1px, transparent 1px); background-size: 20px 20px; width:100%; height:100%; opacity: 0.5;"></div>
|
||||
<svg class="absolute inset-0 w-full h-full text-orange-500 opacity-80" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<path d="M10,80 Q30,60 50,70 T90,30" fill="none" stroke="currentColor" stroke-width="3" stroke-dasharray="5,5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-[#FC4C02] rounded-lg flex items-center justify-center text-white font-bold">S</div>
|
||||
<div>
|
||||
<div class="font-bold text-gray-800">Morning Run</div>
|
||||
<div class="text-xs text-gray-500">Synced via Strava</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
this.container.querySelector('#backBtn').addEventListener('click', () => {
|
||||
this.app.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
120
Hydroflux/app/src/main/assets/js/modules/views/GoalsView.js
Normal file
120
Hydroflux/app/src/main/assets/js/modules/views/GoalsView.js
Normal file
@@ -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 = `
|
||||
<div class="w-full max-w-md bg-white min-h-screen relative overflow-hidden mx-auto bg-gradient-to-br from-blue-50 to-blue-100 flex flex-col p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<button id="backBtn" class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-md">
|
||||
<svg class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<span class="text-lg font-bold text-gray-800">Your Goals</span>
|
||||
<button id="addGoalProto" class="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center shadow-lg hover:bg-blue-600 transition">
|
||||
<svg class="w-6 h-6" 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>
|
||||
</div>
|
||||
|
||||
<!-- Active Goals -->
|
||||
<div class="space-y-4 flex-1 overflow-y-auto pb-20" id="detailGoalsList">
|
||||
${goalsData.map(g => `
|
||||
<div class="bg-white rounded-2xl p-4 shadow-sm flex items-center gap-4 transition-all ${g.completed ? 'opacity-60 grayscale' : ''}">
|
||||
<button class="toggle-goal w-8 h-8 rounded-full border-2 ${g.completed ? 'bg-blue-500 border-blue-500' : 'border-gray-200'} flex items-center justify-center" data-id="${g.id}">
|
||||
${g.completed ? '<svg class="w-4 h-4 text-white" 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>' : ''}
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-gray-800 ${g.completed ? 'line-through text-gray-400' : ''}">${g.text}</div>
|
||||
<div class="text-xs text-gray-400">Daily Goal</div>
|
||||
</div>
|
||||
<button class="delete-goal text-gray-300 hover:text-red-400 p-2" data-id="${g.id}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
${goalsData.length === 0 ? `
|
||||
<div class="text-center text-gray-400 mt-10">
|
||||
No goals set. Tap + to add one!
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Add Goal Input (Hidden by default, shown on click) -->
|
||||
<div id="newGoalPanel" class="hidden absolute bottom-0 left-0 w-full bg-white p-6 rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.1)] z-50">
|
||||
<h3 class="font-bold text-gray-800 mb-4">New Goal</h3>
|
||||
<input type="text" id="newGoalInput" placeholder="e.g. Meditate for 10 mins" class="w-full bg-gray-50 border-none rounded-xl p-4 text-gray-800 font-medium focus:ring-2 focus:ring-blue-200 mb-4 outline-none">
|
||||
<div class="flex gap-3">
|
||||
<button id="cancelAdd" class="flex-1 py-4 rounded-xl bg-gray-100 text-gray-500 font-bold">Cancel</button>
|
||||
<button id="saveGoal" class="flex-1 py-4 rounded-xl bg-blue-500 text-white font-bold shadow-lg shadow-blue-200">Save Goal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
63
Hydroflux/app/src/main/assets/js/modules/views/NotesView.js
Normal file
63
Hydroflux/app/src/main/assets/js/modules/views/NotesView.js
Normal file
@@ -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 = `
|
||||
<div class="w-full max-w-md bg-white min-h-screen relative overflow-hidden mx-auto bg-gradient-to-br from-blue-50 to-blue-100 flex flex-col p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<button id="backBtn" class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-md text-gray-700">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<span class="text-lg font-bold text-gray-800">Notes & Thoughts</span>
|
||||
<button id="saveNoteBtn" class="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center shadow-md">
|
||||
<svg class="w-5 h-5" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="flex-1 bg-white rounded-3xl p-6 shadow-sm border border-gray-100 relative">
|
||||
<textarea id="fullNoteInput" class="w-full h-full resize-none outline-none text-gray-700 text-lg leading-relaxed placeholder-gray-400" placeholder="Write roughly, reflect deeply...">${currentNote}</textarea>
|
||||
|
||||
<div class="absolute bottom-4 right-4 text-xs text-gray-400" id="saveStatus">Auto-saved</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard spacer helper if needed on mobile, but padding usually suffices -->
|
||||
<div class="h-4"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
98
Hydroflux/app/src/main/assets/js/modules/views/SleepView.js
Normal file
98
Hydroflux/app/src/main/assets/js/modules/views/SleepView.js
Normal file
@@ -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 = `
|
||||
<div class="w-full max-w-md bg-white min-h-screen relative overflow-hidden mx-auto bg-gradient-to-br from-blue-50 to-white flex flex-col p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<button id="backBtn" class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-md text-gray-700">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<span class="text-lg font-bold text-gray-800">Sleep Analysis</span>
|
||||
<div class="w-10"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Stat -->
|
||||
<div class="text-center mb-10">
|
||||
<div class="flex items-baseline justify-center gap-1">
|
||||
<span class="text-6xl font-bold text-blue-600">${sleepHours}</span>
|
||||
<span class="text-xl font-medium text-gray-400">h</span>
|
||||
<span class="text-6xl font-bold text-blue-600">${sleepMins}</span>
|
||||
<span class="text-xl font-medium text-gray-400">m</span>
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm mt-2">Total Sleep Duration</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="bg-white rounded-3xl p-6 mb-6 shadow-sm border border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-500 mb-6 uppercase tracking-wider">Sleep Stages</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- REM -->
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-purple-500 font-medium">REM</span>
|
||||
<span class="font-bold text-gray-700">1h 45m</span>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-purple-400" style="width: 25%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deep -->
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-blue-600 font-medium">Deep Sleep</span>
|
||||
<span class="font-bold text-gray-700">2h 10m</span>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-blue-600" style="width: 35%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Light -->
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-blue-400 font-medium">Light Sleep</span>
|
||||
<span class="font-bold text-gray-700">3h 25m</span>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-blue-300" style="width: 40%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Graph -->
|
||||
<div class="bg-white rounded-3xl p-6 border border-gray-100 shadow-sm flex-1">
|
||||
<h3 class="text-sm font-semibold text-gray-500 mb-4 uppercase tracking-wider">Weekly Trend</h3>
|
||||
<div class="h-32 flex items-end justify-between px-2">
|
||||
${[6.5, 7.2, 5.8, 8.0, 7.5, 6.2, 7.3].map((val, i) => `
|
||||
<div class="flex flex-col items-center gap-2 group">
|
||||
<div class="w-8 bg-gradient-to-t from-blue-400 to-blue-600 rounded-t-lg transition-all group-hover:opacity-100 opacity-80"
|
||||
style="height: ${(val / 10) * 100}%"></div>
|
||||
<span class="text-xs text-gray-400 font-medium">${['M', 'T', 'W', 'T', 'F', 'S', 'S'][i]}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
this.container.querySelector('#backBtn').addEventListener('click', () => {
|
||||
this.app.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
196
Hydroflux/app/src/main/assets/js/modules/views/WaterView.js
Normal file
196
Hydroflux/app/src/main/assets/js/modules/views/WaterView.js
Normal file
@@ -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 = `
|
||||
<div class="w-full max-w-md bg-white min-h-screen relative mx-auto bg-gradient-to-br from-blue-50 to-blue-100 flex flex-col h-screen">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center p-6 relative z-10 shrink-0">
|
||||
<button id="backBtn" class="w-10 h-10 rounded-full bg-white bg-opacity-50 backdrop-blur-md flex items-center justify-center shadow-sm">
|
||||
<svg class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
<span class="text-lg font-bold text-gray-800">Hydration</span>
|
||||
<div class="w-10"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Scrollable Area -->
|
||||
<div class="flex-1 overflow-y-auto pb-32">
|
||||
|
||||
<!-- Main Visualization (Circular) -->
|
||||
<div class="flex flex-col items-center justify-center py-6">
|
||||
<div class="relative w-64 h-64 rounded-full border-8 border-white shadow-2xl overflow-hidden bg-blue-50">
|
||||
<!-- Water Fill -->
|
||||
<div class="absolute bottom-0 left-0 w-full water-container transition-all duration-700"
|
||||
style="height: ${percentage}%; background: linear-gradient(180deg, #60A5FA 0%, #3B82F6 100%);">
|
||||
<svg class="absolute top-0 left-0 w-full wave-animation" viewBox="0 0 1200 120" preserveAspectRatio="none" style="height: 40px; transform: translateY(-50%); width: 200%;">
|
||||
<path d="M0,50 Q300,10 600,50 T1200,50 L1200,120 L0,120 Z" fill="#60A5FA" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Center Text Overlay -->
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center z-10">
|
||||
<div class="text-5xl font-bold ${percentage > 50 ? 'text-white' : 'text-blue-600'} drop-shadow-md transition-colors duration-500">${data.current.toFixed(1)}L</div>
|
||||
<div class="font-medium text-sm mt-1 uppercase tracking-wide ${percentage > 50 ? 'text-blue-100' : 'text-gray-500'} transition-colors duration-500">of ${data.goal.toFixed(1)}L Goal</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Section -->
|
||||
<div class="px-6 mt-4">
|
||||
<h3 class="font-bold text-gray-800 text-lg mb-4">Today's History</h3>
|
||||
<div class="space-y-3" id="historyList">
|
||||
${this.history.length === 0 ?
|
||||
'<div class="text-center text-gray-400 py-4 text-sm">No drinks logged yet today.</div>' :
|
||||
this.history.slice().reverse().map(entry => `
|
||||
<div class="flex items-center justify-between p-4 rounded-xl bg-white shadow-sm border border-blue-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-700">Water</div>
|
||||
<div class="text-xs text-gray-400">${new Date(entry.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-bold text-gray-800">+${entry.amount}ml</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Controls -->
|
||||
<div class="absolute bottom-6 left-6 right-6 z-20 bg-white rounded-3xl shadow-xl p-4 border border-blue-50">
|
||||
<div class="flex justify-between items-center">
|
||||
<button class="w-14 h-14 rounded-2xl bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-gray-200 active:scale-95 transition-transform" id="removeWater">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path></svg>
|
||||
</button>
|
||||
|
||||
<button class="w-20 h-20 rounded-full bg-blue-500 shadow-xl shadow-blue-200 hover:shadow-blue-300 flex items-center justify-center transform hover:scale-105 active:scale-95 transition-all -mt-8 border-4 border-white" id="addWater">
|
||||
<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 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
|
||||
</button>
|
||||
|
||||
<!-- Drink Size Selector -->
|
||||
<div class="flex flex-col items-center gap-1 cursor-pointer group" id="drinkSizeBtn">
|
||||
<span class="text-[10px] text-gray-500 font-medium uppercase group-hover:text-blue-500 transition-colors">Size</span>
|
||||
<div class="w-14 h-14 rounded-2xl border-2 border-blue-100 flex items-center justify-center text-blue-500 font-bold bg-blue-50 group-hover:bg-blue-100 transition-colors relative text-sm">
|
||||
${drinkSize}
|
||||
<div class="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg class="w-2 h-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size Selection Modal (Hidden) -->
|
||||
<div id="sizeModal" class="hidden absolute bottom-0 left-0 w-full z-50 h-full flex flex-col justify-end">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-20 backdrop-blur-sm" id="closeModalMask"></div>
|
||||
<div class="bg-white rounded-t-3xl p-8 shadow-[0_-10px_40px_rgba(0,0,0,0.1)] relative animate-slide-up z-50">
|
||||
<div class="w-12 h-1 bg-gray-200 rounded-full mx-auto mb-6"></div>
|
||||
<h3 class="font-bold text-gray-800 text-lg mb-4 text-center">Select Amount</h3>
|
||||
|
||||
<!-- Custom Input -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<input type="number" id="customAmountInput" placeholder="Custom ml" class="flex-1 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-lg font-bold text-gray-800 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<button id="applyCustom" class="px-6 py-3 bg-blue-500 text-white font-bold rounded-xl shadow-md">Set</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3 mb-6">
|
||||
${[100, 250, 330, 500, 750, 1000].map(size => `
|
||||
<button class="size-option py-3 rounded-xl border-2 ${size === drinkSize ? 'border-blue-500 bg-blue-50 text-blue-600' : 'border-gray-100 text-gray-600 hover:bg-gray-50'} font-bold transition-all text-sm" data-size="${size}">
|
||||
${size}ml
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button id="closeModal" class="w-full py-3 rounded-xl bg-gray-100 text-gray-600 font-bold">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||
.animate-slide-up { animation: slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user