Files
HydroFlux/Hydroflux/REPLICATION_GUIDE.md
2026-02-09 20:48:36 +11:00

16 KiB

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

{
  "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

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

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

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

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

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

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

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

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