897 lines
57 KiB
TypeScript
897 lines
57 KiB
TypeScript
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { StorageService } from '../services/storageService';
|
|
import { AirQualityService } from '../services/airQualityService';
|
|
import { interpretDream, createHealthAssistant } from '../services/geminiService';
|
|
import { HealthLog, AQIData, WeatherData, ChatMessage } from '../types';
|
|
import {
|
|
Heart, Droplets, Smile, Moon, Plus, Minus, Loader2, Wind,
|
|
CloudFog, MapPin, Sun, CloudRain, CloudLightning, Activity,
|
|
Leaf, Thermometer, Umbrella, Eye, Dumbbell, Flower2,
|
|
Sprout, Timer, Apple, Users, ShieldCheck, HeartPulse, Play,
|
|
Waves, Sparkles, Brain, Zap, BatteryMedium, CloudMoon, BookOpen, Quote,
|
|
ChevronRight, X, Send, BarChart, MessageSquare, Info, GripVertical, Pause, RotateCcw, CheckCircle, Languages, Mic
|
|
} from 'lucide-react';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import confetti from 'canvas-confetti';
|
|
import { Button } from '../components/ui/Button';
|
|
import { BarChart as RechartsBarChart, Bar, ResponsiveContainer, XAxis, Tooltip, Cell, YAxis } from 'recharts';
|
|
|
|
const QUOTES = [
|
|
{ en: "The earth does not belong to us: we belong to the earth.", ne: "पृथ्वी हाम्रो होइन, हामी पृथ्वीका हौं।" },
|
|
{ en: "A healthy body houses a healthy mind.", ne: "स्वस्थ शरीरमा नै स्वस्थ मनको बास हुन्छ।" },
|
|
{ en: "Look deep into nature, and then you will understand everything better.", ne: "प्रकृतिमा गहिरिएर हेर्नुहोस्, अनि सबै कुरा राम्ररी बुझ्नुहुनेछ।" },
|
|
];
|
|
|
|
const YOGA_POSES = [
|
|
{
|
|
id: 'yoga_1',
|
|
name: 'Surya Namaskar',
|
|
neName: 'सूर्य नमस्कार',
|
|
icon: Sun,
|
|
benefits: 'Full body workout, improves blood circulation.',
|
|
neBenefits: 'पूरा शरीरको व्यायाम, रक्तसञ्चारमा सुधार।',
|
|
steps: '12 steps combining 7 different asanas.',
|
|
neSteps: '७ विभिन्न आसनहरू मिलाएर १२ चरणहरू।',
|
|
detailedSteps: [
|
|
"Stand straight, palms folded in prayer pose. Breathe in.",
|
|
"Raise arms overhead, arch back slightly. Breathe out.",
|
|
"Bend forward, touch your feet. Keep knees straight.",
|
|
"Step right leg back, look up (Equestrian Pose).",
|
|
"Step left leg back into Plank pose. Keep body straight.",
|
|
"Lower knees, chest, and chin to floor (Ashtanga Namaskar).",
|
|
"Slide forward into Cobra pose. Look up.",
|
|
"Lift hips into Inverted V (Mountain pose).",
|
|
"Step right foot forward between hands.",
|
|
"Step left foot forward, bend down.",
|
|
"Raise arms overhead, stretch back.",
|
|
"Return to standing prayer pose."
|
|
],
|
|
detailedStepsNe: [
|
|
"सीधा उभिनुहोस्, हातहरू जोडेर प्रार्थना मुद्रामा। सास लिनुहोस्।",
|
|
"हातहरू टाउको माथि उठाउनुहोस्, अलिकति पछाडि ढल्किनुहोस्। सास छोड्नुहोस्।",
|
|
"अगाडि झुक्नुहोस्, खुट्टा छुनुहोस्। घुँडाहरू सीधा राख्नुहोस्।",
|
|
"दायाँ खुट्टा पछाडि सार्नुहोस्, माथि हेर्नुहोस् (अश्व सञ्चालन आसन)।",
|
|
"बायाँ खुट्टा पछाडि सार्नुहोस् र प्ल्याङ्क पोजमा जानुहोस्। शरीर सीधा राख्नुहोस्।",
|
|
"घुँडा, छाती र चिउँडो भुइँमा राख्नुहोस् (अष्टाङ्ग नमस्कार)।",
|
|
"अगाडि सर्दै कोब्रा पोज (भुजङ्गासन) मा जानुहोस्। माथि हेर्नुहोस्।",
|
|
"कम्मर माथि उठाउनुहोस् र उल्टो V आकार (पर्वतासन) बनाउनुहोस्।",
|
|
"दायाँ खुट्टा हातहरूको बीचमा अगाडि ल्याउनुहोस्।",
|
|
"बायाँ खुट्टा अगाडि ल्याउनुहोस्, तल झुक्नुहोस्।",
|
|
"हातहरू माथि उठाउनुहोस्, पछाडि तन्किनुहोस्।",
|
|
"प्रार्थना मुद्रामा फर्कनुहोस्।"
|
|
]
|
|
},
|
|
{
|
|
id: 'yoga_2',
|
|
name: 'Pranayama',
|
|
neName: 'प्राणायाम',
|
|
icon: Wind,
|
|
benefits: 'Reduces stress, improves lung capacity.',
|
|
neBenefits: 'तनाव कम गर्छ, फोक्सोको क्षमता बढाउँछ।',
|
|
steps: 'Breath awareness and controlled breathing.',
|
|
neSteps: 'श्वासप्रश्वासको जागरूकता र नियन्त्रित श्वास।',
|
|
detailedSteps: [
|
|
"Sit in a comfortable cross-legged position.",
|
|
"Close your eyes and relax your shoulders.",
|
|
"Inhale deeply through your nose for 4 seconds.",
|
|
"Hold your breath for 4 seconds.",
|
|
"Exhale slowly through your nose for 6 seconds.",
|
|
"Repeat this cycle for 5 minutes."
|
|
],
|
|
detailedStepsNe: [
|
|
"आरामदायी पलेँटी कसरे बस्नुहोस्।",
|
|
"आँखा बन्द गर्नुहोस् र काँधहरूलाई खुकुलो छोड्नुहोस्।",
|
|
"नाकबाट ४ सेकेन्डसम्म गहिरो सास लिनुहोस्।",
|
|
"४ सेकेन्डसम्म सास रोक्नुहोस्।",
|
|
"बिस्तारै ६ सेकेन्डसम्म नाकबाट सास छोड्नुहोस्।",
|
|
"यो प्रक्रिया ५ मिनेटसम्म दोहोर्याउनुहोस्।"
|
|
]
|
|
},
|
|
{
|
|
id: 'yoga_3',
|
|
name: 'Vrikshasana',
|
|
neName: 'वृक्षासन',
|
|
icon: Sprout,
|
|
benefits: 'Improves balance and leg strength.',
|
|
neBenefits: 'सन्तुलन र खुट्टाको बल सुधार गर्छ।',
|
|
steps: 'Stand on one leg, foot on inner thigh.',
|
|
neSteps: 'एउटा खुट्टामा उभिनुहोस्, अर्को पाइतला तिघ्रामा राख्नुहोस्।',
|
|
detailedSteps: [
|
|
"Stand tall with feet together.",
|
|
"Shift weight to left leg.",
|
|
"Place right foot on inner left thigh.",
|
|
"Bring hands to prayer position at chest.",
|
|
"Raise hands above head, keep elbows straight.",
|
|
"Focus on a point in front of you. Hold.",
|
|
"Slowly lower hands and leg. Repeat other side."
|
|
],
|
|
detailedStepsNe: [
|
|
"खुट्टाहरू जोडेर सीधा उभिनुहोस्।",
|
|
"तौल बायाँ खुट्टामा सार्नुहोस्।",
|
|
"दायाँ खुट्टाको पाइतला बायाँ तिघ्राको भित्री भागमा राख्नुहोस्।",
|
|
"हातहरू छातीको अगाडि नमस्कार मुद्रामा ल्याउनुहोस्।",
|
|
"हातहरू टाउको माथि उठाउनुहोस्, कुहिनो सीधा राख्नुहोस्।",
|
|
"अगाडि एउटा बिन्दुमा ध्यान केन्द्रित गर्नुहोस्। अडिनुहोस्।",
|
|
"बिस्तारै हात र खुट्टा तल झार्नुहोस्। अर्को तर्फ दोहोर्याउनुहोस्।"
|
|
]
|
|
}
|
|
];
|
|
|
|
const AYURVEDA_TIPS = [
|
|
{
|
|
title: 'Dinacharya (Daily Routine)',
|
|
neTitle: 'दिनचर्या',
|
|
icon: Timer,
|
|
tip: 'Wake up before sunrise (Brahma Muhurta) to synchronize with nature.',
|
|
neTip: 'प्रकृतिसँग तालमेल मिलाउन सूर्योदयभन्दा अगाडि (ब्रह्म मुहूर्त) उठ्नुहोस्।'
|
|
},
|
|
{
|
|
title: 'Warm Water Secret',
|
|
neTitle: 'तातो पानीको रहस्य',
|
|
icon: Droplets,
|
|
tip: 'Sip warm water throughout the day to boost metabolism and digestion.',
|
|
neTip: 'मेटाबोलिज्म र पाचन बढाउन दिनभरि तातो पानी पिउनुहोस्।'
|
|
},
|
|
{
|
|
title: 'Herbal Power',
|
|
neTitle: 'जडीबुटी शक्ति',
|
|
icon: Leaf,
|
|
tip: 'Chew a few Tulsi leaves daily to strengthen your immune system.',
|
|
neTip: 'रोग प्रतिरोधात्मक क्षमता बलियो बनाउन दिनहुँ तुलसीका पातहरू चपाउनुहोस्।'
|
|
}
|
|
];
|
|
|
|
const LONGEVITY_PILLARS = [
|
|
{ en: "Active Lifestyle", ne: "सक्रिय जीवनशैली", icon: Dumbbell, desc: "Walk at least 30 mins daily in nature.", neDesc: "प्रकृतिमा दैनिक कम्तीमा ३० मिनेट हिड्नुहोस्।" },
|
|
{ en: "Sattvic Diet", ne: "सात्त्विक आहार", icon: Apple, desc: "Eat fresh, seasonal, and plant-based foods.", neDesc: "ताजा, मौसमी र वनस्पतिमा आधारित खाना खानुहोस्।" },
|
|
{ en: "Social Connection", ne: "सामाजिक सम्बन्ध", icon: Users, desc: "Talk to loved ones daily to reduce cortisol.", neDesc: "तनाव कम गर्न दैनिक प्रियजनहरूसँग कुरा गर्नुहोस्।" },
|
|
{ en: "Consistent Sleep", ne: "नियमित निद्रा", icon: Moon, desc: "Rest is the best medicine. Sleep 7-8 hours.", neDesc: "आराम नै उत्तम औषधि हो। ७-८ घण्टा सुत्नुहोस्।" }
|
|
];
|
|
|
|
// --- YOGA SESSION OVERLAY COMPONENT ---
|
|
const YogaSession = ({ pose, onClose }: { pose: any, onClose: () => void }) => {
|
|
const { t } = useLanguage();
|
|
const [stepIndex, setStepIndex] = useState(0);
|
|
const [timer, setTimer] = useState(0);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isFinished, setIsFinished] = useState(false);
|
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
const [sessionLang, setSessionLang] = useState<'en' | 'ne' | null>(null);
|
|
const [awardMessage, setAwardMessage] = useState<string>('');
|
|
|
|
const timerRef = useRef<any>(null);
|
|
const steps = sessionLang === 'ne' ? (pose.detailedStepsNe || pose.detailedSteps) : pose.detailedSteps;
|
|
|
|
// Enhanced Voice Feedback Function
|
|
const speakStep = useCallback((text: string) => {
|
|
if (!window.speechSynthesis) return;
|
|
window.speechSynthesis.cancel();
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
utterance.rate = 0.9;
|
|
utterance.pitch = 1;
|
|
utterance.volume = 1;
|
|
|
|
// Find best voice for Nepali
|
|
if (sessionLang === 'ne') {
|
|
const voices = window.speechSynthesis.getVoices();
|
|
// Try Nepali specific, then Hindi (often works for Devanagari), then generic
|
|
const neVoice = voices.find(v => v.lang.includes('ne')) ||
|
|
voices.find(v => v.lang.includes('hi')) ||
|
|
voices.find(v => v.lang.includes('IN'));
|
|
if (neVoice) utterance.voice = neVoice;
|
|
utterance.lang = 'hi-IN'; // Fallback hint
|
|
} else {
|
|
utterance.lang = 'en-US';
|
|
}
|
|
|
|
utterance.onstart = () => setIsSpeaking(true);
|
|
utterance.onend = () => setIsSpeaking(false);
|
|
|
|
window.speechSynthesis.speak(utterance);
|
|
}, [sessionLang]);
|
|
|
|
// Ensure voices are loaded (Chrome quirk)
|
|
useEffect(() => {
|
|
window.speechSynthesis.getVoices();
|
|
}, []);
|
|
|
|
// Handle initial start
|
|
useEffect(() => {
|
|
if (sessionLang && !isFinished && !isPlaying) {
|
|
setIsPlaying(true);
|
|
setTimer(0);
|
|
|
|
// Intro Voice Instruction
|
|
const intro = sessionLang === 'ne'
|
|
? `${pose.neName} सुरु हुँदैछ। तयार हुनुहोस्।`
|
|
: `Starting ${pose.name}. Get ready.`;
|
|
|
|
speakStep(intro);
|
|
|
|
// Wait for intro to finish approx before starting steps logic
|
|
setTimeout(() => {
|
|
const stepOne = sessionLang === 'ne'
|
|
? `चरण एक। ${steps[0]}`
|
|
: `Step 1. ${steps[0]}`;
|
|
speakStep(stepOne);
|
|
}, 3000);
|
|
}
|
|
}, [sessionLang, pose, speakStep, steps]);
|
|
|
|
// Voice Control Listener
|
|
useEffect(() => {
|
|
const handleVoiceControl = (e: CustomEvent) => {
|
|
if (!sessionLang || isFinished) return;
|
|
|
|
const { action } = e.detail;
|
|
if (action === 'next') {
|
|
if (stepIndex < steps.length - 1) {
|
|
setStepIndex(prev => prev + 1);
|
|
} else {
|
|
finishSession();
|
|
}
|
|
} else if (action === 'prev') {
|
|
if (stepIndex > 0) {
|
|
setStepIndex(prev => prev - 1);
|
|
}
|
|
} else if (action === 'repeat') {
|
|
const txt = sessionLang === 'ne'
|
|
? `फेरि भन्दै। ${steps[stepIndex]}`
|
|
: `Repeating. ${steps[stepIndex]}`;
|
|
speakStep(txt);
|
|
} else if (action === 'exit') {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('rudraksha-yoga-control' as any, handleVoiceControl);
|
|
return () => window.removeEventListener('rudraksha-yoga-control' as any, handleVoiceControl);
|
|
}, [sessionLang, stepIndex, steps, isFinished, onClose, speakStep]);
|
|
|
|
// Effect for step changes (Trigger speech when stepIndex updates)
|
|
useEffect(() => {
|
|
if (isPlaying && !isFinished && sessionLang) {
|
|
// Don't speak immediately on mount (handled by intro effect), only on change
|
|
if (timer > 0 || stepIndex > 0) {
|
|
const prefix = sessionLang === 'ne' ? 'चरण' : 'Step';
|
|
const num = sessionLang === 'ne'
|
|
? String(stepIndex + 1).replace(/[0-9]/g, d => "०१२३४५६७८९"[Number(d)])
|
|
: (stepIndex + 1);
|
|
|
|
speakStep(`${prefix} ${num}. ${steps[stepIndex]}`);
|
|
}
|
|
}
|
|
}, [stepIndex, sessionLang]);
|
|
|
|
// Timer Logic
|
|
useEffect(() => {
|
|
if (isPlaying && !isFinished) {
|
|
timerRef.current = setInterval(() => {
|
|
setTimer(prev => prev + 1);
|
|
}, 1000);
|
|
} else {
|
|
clearInterval(timerRef.current);
|
|
}
|
|
return () => clearInterval(timerRef.current);
|
|
}, [isPlaying, isFinished]);
|
|
|
|
// Cleanup speech on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
window.speechSynthesis.cancel();
|
|
};
|
|
}, []);
|
|
|
|
const nextStep = () => {
|
|
if (stepIndex < steps.length - 1) {
|
|
setStepIndex(prev => prev + 1);
|
|
} else {
|
|
finishSession();
|
|
}
|
|
};
|
|
|
|
const prevStep = () => {
|
|
if (stepIndex > 0) {
|
|
setStepIndex(prev => prev - 1);
|
|
}
|
|
};
|
|
|
|
const finishSession = async () => {
|
|
setIsFinished(true);
|
|
setIsPlaying(false);
|
|
const finalText = sessionLang === 'ne' ? "सत्र पूरा भयो। नमस्ते।" : "Session complete. Namaste.";
|
|
speakStep(finalText);
|
|
|
|
// Awarding Logic via StorageService
|
|
const result = await StorageService.trackYogaSession(pose.id);
|
|
setAwardMessage(result.message);
|
|
|
|
if (result.awarded) {
|
|
confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 }, colors: ['#a855f7', '#6366f1'] });
|
|
}
|
|
};
|
|
|
|
if (!sessionLang) {
|
|
return (
|
|
<div className="fixed inset-0 z-[200] bg-black/95 flex flex-col items-center justify-center p-6 animate-in fade-in duration-500">
|
|
<div className="w-full max-w-md bg-white dark:bg-gray-900 rounded-[2rem] p-8 text-center space-y-8">
|
|
<h2 className="text-2xl font-black text-gray-900 dark:text-white uppercase tracking-tighter">Select Instruction Language</h2>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Button onClick={() => setSessionLang('en')} className="h-16 text-lg font-black bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl">
|
|
English
|
|
</Button>
|
|
<Button onClick={() => setSessionLang('ne')} className="h-16 text-lg font-black bg-red-600 hover:bg-red-700 text-white rounded-2xl">
|
|
नेपाली (Nepali)
|
|
</Button>
|
|
</div>
|
|
<Button variant="ghost" onClick={onClose} className="text-gray-500 hover:text-white">Cancel</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const formatTimer = (t: number) => {
|
|
const m = Math.floor(t / 60);
|
|
const s = t % 60;
|
|
if (sessionLang === 'ne') {
|
|
const timeStr = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
return timeStr.replace(/[0-9]/g, d => "०१२३४५६७८९"[Number(d)]);
|
|
}
|
|
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[200] bg-black/95 flex flex-col items-center justify-center p-6 animate-in fade-in duration-500">
|
|
<button onClick={onClose} className="absolute top-6 right-6 p-4 bg-white/10 rounded-full hover:bg-red-500 hover:text-white transition-all text-white"><X size={24}/></button>
|
|
|
|
{!isFinished ? (
|
|
<div className="w-full max-w-2xl text-center space-y-12">
|
|
<div className="space-y-4">
|
|
<div className="w-24 h-24 bg-indigo-500/20 rounded-full flex items-center justify-center mx-auto border-4 border-indigo-500 shadow-[0_0_40px_rgba(99,102,241,0.4)] animate-pulse">
|
|
<pose.icon size={48} className="text-indigo-400"/>
|
|
</div>
|
|
<h2 className="text-4xl font-black text-white uppercase italic tracking-tighter">{sessionLang === 'ne' ? pose.neName : pose.name}</h2>
|
|
<div className="text-indigo-300 font-bold uppercase tracking-widest text-sm">
|
|
{sessionLang === 'ne'
|
|
? `चरण ${String(stepIndex + 1).replace(/[0-9]/g, d => "०१२३४५६७८९"[Number(d)])} / ${String(steps.length).replace(/[0-9]/g, d => "०१२३४५६७८९"[Number(d)])}`
|
|
: `Step ${stepIndex + 1} / ${steps.length}`}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white/5 border border-white/10 p-10 rounded-[3rem] relative overflow-hidden">
|
|
<div className="absolute top-0 left-0 w-full h-1 bg-white/10">
|
|
<div className="h-full bg-indigo-500 transition-all duration-300" style={{ width: `${((stepIndex + 1) / steps.length) * 100}%` }}></div>
|
|
</div>
|
|
<p className="text-2xl md:text-4xl font-medium text-white leading-relaxed font-serif">"{steps[stepIndex]}"</p>
|
|
<div className="mt-8 flex justify-center items-center gap-2 text-indigo-400 font-mono text-xl">
|
|
<Timer size={20} className={isPlaying ? "animate-spin-slow" : ""}/>
|
|
<span>{formatTimer(timer)}</span>
|
|
</div>
|
|
{/* Voice Control Hint */}
|
|
<div className="absolute bottom-4 left-0 right-0 text-center opacity-40">
|
|
<p className="text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-2">
|
|
<Mic size={12}/>
|
|
{sessionLang === 'ne'
|
|
? "भन्नुहोस्: अर्को, अघिल्लो, फेरि"
|
|
: 'Say "Next", "Back", "Repeat"'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-center gap-6">
|
|
<Button onClick={prevStep} disabled={stepIndex === 0} variant="secondary" className="w-16 h-16 rounded-full flex items-center justify-center bg-white/10 text-white border-none hover:bg-white/20"><ChevronRight size={24} className="rotate-180"/></Button>
|
|
<Button onClick={() => setIsPlaying(!isPlaying)} className={`w-24 h-24 rounded-[2rem] flex items-center justify-center shadow-2xl transition-all ${isPlaying ? 'bg-yellow-500 text-black' : 'bg-green-600 text-white'}`}>
|
|
{isPlaying ? <Pause size={32} fill="currentColor"/> : <Play size={32} fill="currentColor"/>}
|
|
</Button>
|
|
<Button onClick={nextStep} variant="secondary" className="w-16 h-16 rounded-full flex items-center justify-center bg-white/10 text-white border-none hover:bg-white/20"><ChevronRight size={24}/></Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center space-y-8 animate-in zoom-in duration-500">
|
|
<div className="w-32 h-32 bg-green-500 rounded-full flex items-center justify-center mx-auto shadow-2xl shadow-green-500/40">
|
|
<CheckCircle size={64} className="text-white"/>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-5xl font-black text-white uppercase italic tracking-tighter mb-2">{sessionLang === 'ne' ? "नमस्ते" : "Namaste"}</h2>
|
|
<p className="text-gray-400 text-lg">{sessionLang === 'ne' ? "सत्र सफलतापूर्वक सम्पन्न भयो।" : "Session Completed Successfully."}</p>
|
|
</div>
|
|
<div className="bg-white/10 p-6 rounded-3xl border border-white/10">
|
|
<div className="text-sm font-black text-yellow-400 uppercase tracking-widest mb-1">Status</div>
|
|
<div className="text-xl font-bold text-white">{awardMessage}</div>
|
|
</div>
|
|
<div className="flex gap-4 justify-center">
|
|
<Button onClick={() => { setIsFinished(false); setStepIndex(0); setTimer(0); setIsPlaying(true); }} variant="secondary" className="bg-white/10 text-white border-none">
|
|
<RotateCcw size={18} className="mr-2"/> Repeat
|
|
</Button>
|
|
<Button onClick={onClose} className="bg-white text-black font-black">
|
|
Return to Hub
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Health: React.FC = () => {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const { language, t } = useLanguage();
|
|
|
|
const [activeTab, setActiveTab] = useState<'climate' | 'personal' | 'yog' | 'ancient' | 'dream'>('climate');
|
|
const [log, setLog] = useState<HealthLog | null>(null);
|
|
const [aqiData, setAqiData] = useState<AQIData | null>(null);
|
|
const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
|
|
const [loadingClimate, setLoadingClimate] = useState(false);
|
|
const [loadingLog, setLoadingLog] = useState(false);
|
|
const [quote, setQuote] = useState(QUOTES[0]);
|
|
|
|
const [dreamInput, setDreamInput] = useState('');
|
|
const [dreamResult, setDreamResult] = useState<{
|
|
folklore: { en: string, ne: string },
|
|
psychology: { en: string, ne: string },
|
|
symbol: string
|
|
} | null>(null);
|
|
const [isDreaming, setIsDreaming] = useState(false);
|
|
|
|
// Yoga Interaction State
|
|
const [isSelectingPose, setIsSelectingPose] = useState(false);
|
|
const [activeYogaPose, setActiveYogaPose] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchClimate = async () => {
|
|
setLoadingClimate(true);
|
|
try {
|
|
const [aqi, weather] = await Promise.all([
|
|
AirQualityService.getAQI(),
|
|
AirQualityService.getWeather()
|
|
]);
|
|
setAqiData(aqi);
|
|
setWeatherData(weather);
|
|
} catch (e) {
|
|
console.error("Climate Fetch Error:", e);
|
|
} finally {
|
|
setLoadingClimate(false);
|
|
}
|
|
};
|
|
fetchClimate();
|
|
setQuote(QUOTES[Math.floor(Math.random() * QUOTES.length)]);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'personal' && !log) {
|
|
const fetchLog = async () => {
|
|
setLoadingLog(true);
|
|
try {
|
|
const data = await StorageService.getHealthLog(today);
|
|
setLog(data);
|
|
} finally {
|
|
setLoadingLog(false);
|
|
}
|
|
};
|
|
fetchLog();
|
|
}
|
|
}, [activeTab, log, today]);
|
|
|
|
const updateLog = async (newLog: HealthLog) => {
|
|
setLog(newLog);
|
|
await StorageService.saveHealthLog(newLog);
|
|
if (newLog.waterGlasses === 8) {
|
|
StorageService.addPoints(10);
|
|
confetti({
|
|
particleCount: 150,
|
|
spread: 70,
|
|
origin: { y: 0.6 },
|
|
colors: ['#3b82f6', '#60a5fa', '#93c5fd']
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDreamInterpret = async () => {
|
|
if (!dreamInput.trim()) return;
|
|
setIsDreaming(true);
|
|
setDreamResult(null);
|
|
try {
|
|
const result = await interpretDream(dreamInput);
|
|
setDreamResult(result);
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setIsDreaming(false);
|
|
}
|
|
};
|
|
|
|
const getWeatherIcon = (condition: string) => {
|
|
switch(condition) {
|
|
case 'Sunny': return <Sun size={64} className="text-yellow-400 animate-float"/>;
|
|
case 'Rainy': return <CloudRain size={64} className="text-blue-400 animate-float"/>;
|
|
case 'Stormy': return <CloudLightning size={64} className="text-purple-500 animate-float"/>;
|
|
case 'Foggy': return <CloudFog size={64} className="text-gray-400 animate-float"/>;
|
|
default: return <Sun size={64} className="text-orange-400 animate-float"/>;
|
|
}
|
|
};
|
|
|
|
const getWeatherGradient = (condition: string) => {
|
|
switch(condition) {
|
|
case 'Sunny': return "from-blue-400 to-blue-200 dark:from-blue-800 dark:to-blue-600";
|
|
case 'Rainy': return "from-slate-700 to-slate-500 dark:from-gray-800 dark:to-gray-700";
|
|
case 'Foggy': return "from-gray-400 to-gray-200 dark:from-gray-700 dark:to-gray-600";
|
|
default: return "from-blue-500 to-cyan-400";
|
|
}
|
|
};
|
|
|
|
const getMoodEmoji = (mood: string) => {
|
|
switch(mood) {
|
|
case 'Happy': return { emoji: '😊', color: 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-600' };
|
|
case 'Neutral': return { emoji: '😐', color: 'bg-gray-100 dark:bg-gray-700 text-gray-500' };
|
|
case 'Stressed': return { emoji: '😫', color: 'bg-red-100 dark:bg-red-900/40 text-red-600' };
|
|
case 'Tired': return { emoji: '😴', color: 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600' };
|
|
default: return { emoji: '😐', color: 'bg-gray-100 dark:bg-gray-700 text-gray-500' };
|
|
}
|
|
};
|
|
|
|
const isWaterOverflowing = log && log.waterGlasses >= 8;
|
|
|
|
const handleStartFlow = () => {
|
|
setIsSelectingPose(true);
|
|
};
|
|
|
|
const handleSelectPose = (pose: any) => {
|
|
setIsSelectingPose(false);
|
|
setActiveYogaPose(pose);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-10 pb-20 relative">
|
|
{activeYogaPose && <YogaSession pose={activeYogaPose} onClose={() => setActiveYogaPose(null)} />}
|
|
|
|
<header className="flex flex-col xl:flex-row justify-between items-start xl:items-center gap-6">
|
|
<div>
|
|
<h1 className="text-4xl font-black text-gray-900 dark:text-white flex items-center gap-3 italic">
|
|
<HeartPulse className="text-teal-600 animate-pulse" size={36} /> {t("Wellness Centre", "Wellness Centre")}
|
|
</h1>
|
|
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium mt-1">
|
|
{t("Harmonizing ancient wisdom with modern health analytics.", "Harmonizing ancient wisdom with modern health analytics.")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-xl p-2 rounded-[2rem] flex flex-wrap gap-2 shadow-xl border border-white dark:border-gray-700">
|
|
{[
|
|
{ id: 'climate', icon: Wind, label: 'Environment', color: 'text-teal-500' },
|
|
{ id: 'personal', icon: BatteryMedium, label: 'Daily Log', color: 'text-red-500' },
|
|
{ id: 'yog', icon: Waves, label: 'Yoga Flow', color: 'text-indigo-500' },
|
|
{ id: 'ancient', icon: Sparkles, label: 'Wisdom', color: 'text-amber-500' },
|
|
{ id: 'dream', icon: CloudMoon, label: 'Sapana', color: 'text-purple-500' }
|
|
].map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as any)}
|
|
className={`px-6 py-3 rounded-2xl text-sm font-black flex items-center gap-3 transition-all ${activeTab === tab.id ? 'bg-white dark:bg-gray-700 shadow-xl scale-105 text-gray-900 dark:text-white' : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}`}
|
|
aria-pressed={activeTab === tab.id}
|
|
>
|
|
<tab.icon size={20} className={tab.color}/> {t(tab.label, tab.label)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="min-h-[400px]">
|
|
{/* CLIMATE VIEW */}
|
|
{activeTab === 'climate' && (
|
|
<div className="animate-in fade-in slide-in-from-bottom-8 duration-700">
|
|
{loadingClimate ? (
|
|
<div className="flex flex-col items-center justify-center py-32 gap-4">
|
|
<Loader2 className="animate-spin text-teal-600 w-12 h-12" />
|
|
<p className="text-gray-500 font-bold animate-pulse uppercase tracking-widest text-xs">Locating Environment...</p>
|
|
</div>
|
|
) : weatherData && aqiData ? (
|
|
<div className="space-y-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<div className={`relative overflow-hidden rounded-[3rem] p-10 text-white shadow-2xl bg-gradient-to-br ${getWeatherGradient(weatherData.condition)} group`}>
|
|
<div className="absolute top-[-50px] right-[-50px] w-48 h-48 bg-white/10 rounded-full blur-3xl group-hover:bg-white/20 transition-all"></div>
|
|
<div className="relative z-10 flex justify-between items-start">
|
|
<div>
|
|
<h2 className="text-xl font-black opacity-90 flex items-center gap-2 uppercase tracking-widest"><MapPin size={20}/> {weatherData.location}</h2>
|
|
<p className="text-sm font-bold opacity-75 mt-1">{new Date().toDateString()}</p>
|
|
<div className="mt-12">
|
|
<h1 className="text-8xl font-black tracking-tighter drop-shadow-lg">{weatherData.temp}°</h1>
|
|
<p className="text-2xl font-bold mt-2 uppercase italic tracking-tighter">{t(weatherData.condition, weatherData.condition)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-6">
|
|
{getWeatherIcon(weatherData.condition)}
|
|
<div className="bg-white/20 backdrop-blur-xl rounded-[2rem] p-6 text-sm font-black space-y-2 min-w-[160px] border border-white/20 shadow-2xl">
|
|
<div className="flex justify-between border-b border-white/10 pb-1"><span>{t("Humidity", "Humidity")}</span> <span>{weatherData.humidity}%</span></div>
|
|
<div className="flex justify-between border-b border-white/10 pb-1"><span>{t("Wind", "Wind")}</span> <span>{weatherData.windSpeed} km/h</span></div>
|
|
<div className="flex justify-between"><span>{t("Feels Like", "Feels Like")}</span> <span>{weatherData.feelsLike}°</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-[3rem] shadow-2xl border-4 border-gray-50 dark:border-gray-700 p-10 flex flex-col justify-between group">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h3 className="text-gray-400 dark:text-gray-500 font-black uppercase tracking-[0.3em] text-xs mb-2">{t("Air Quality Index", "Air Quality Index")}</h3>
|
|
<h2 className="text-6xl font-black text-gray-900 dark:text-white mt-1 tracking-tighter" style={{color: aqiData.color}}>{aqiData.aqi}</h2>
|
|
<span className="inline-block px-5 py-2 rounded-full text-white text-xs font-black uppercase tracking-widest mt-4 shadow-xl" style={{backgroundColor: aqiData.color}}>
|
|
{t(aqiData.status, aqiData.status)}
|
|
</span>
|
|
</div>
|
|
<div className="w-24 h-24 rounded-[2rem] flex items-center justify-center bg-gray-50 dark:bg-gray-700 shadow-inner group-hover:rotate-12 transition-transform duration-500">
|
|
<Wind className="text-teal-500" size={48}/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-10 space-y-5">
|
|
<div className="p-6 bg-gray-50 dark:bg-gray-900/50 rounded-[2rem] flex gap-6 items-center border border-transparent group-hover:border-teal-500/20 transition-all">
|
|
<div className="p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-xl text-blue-500"><CloudFog size={28}/></div>
|
|
<div>
|
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">{t("Dominant Pollutant", "Dominant Pollutant")}</p>
|
|
<p className="text-xl font-black text-gray-900 dark:text-white">{aqiData.pollutant}</p>
|
|
</div>
|
|
</div>
|
|
<div className="p-6 bg-blue-50/50 dark:bg-blue-900/10 rounded-[2rem] flex gap-6 items-center border-2 border-blue-100 dark:border-blue-900/30">
|
|
<div className="p-4 bg-white dark:bg-blue-800 rounded-2xl shadow-xl text-blue-500"><ShieldCheck size={28}/></div>
|
|
<div>
|
|
<p className="text-[10px] font-black text-blue-500 uppercase tracking-widest mb-1">{t("Health Advice", "Health Advice")}</p>
|
|
<p className="text-base font-bold text-blue-900 dark:text-blue-100 leading-tight italic">"{aqiData.advice}"</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-32 opacity-40">
|
|
<MapPin size={48} className="mx-auto mb-4" />
|
|
<p className="font-bold">Weather data unavailable. Please check location permissions.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* DREAM INTERPRETER VIEW */}
|
|
{activeTab === 'dream' && (
|
|
<div className="animate-in fade-in slide-in-from-right-8 duration-700">
|
|
<div className="bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-900 rounded-[3.5rem] p-10 md:p-14 text-white shadow-2xl relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20 animate-pulse-slow"></div>
|
|
<div className="absolute -right-20 -top-20 w-96 h-96 bg-purple-500/20 rounded-full blur-[120px]"></div>
|
|
|
|
<div className="relative z-10 max-w-4xl mx-auto space-y-10">
|
|
<div className="text-center space-y-4">
|
|
<CloudMoon size={64} className="mx-auto text-purple-300 animate-float"/>
|
|
<h2 className="text-5xl font-black uppercase italic tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200">
|
|
{t("Sapana Interpreter", "Sapana Interpreter")}
|
|
</h2>
|
|
<p className="text-indigo-200/80 font-medium text-lg max-w-xl mx-auto">
|
|
{t("Unlock the hidden messages of your subconscious through the lens of ancient Nepali folklore and modern psychology.", "Unlock the hidden messages of your subconscious through the lens of ancient Nepali folklore and modern psychology.")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white/10 backdrop-blur-xl rounded-[2.5rem] p-8 border border-white/10 shadow-2xl">
|
|
<textarea
|
|
value={dreamInput}
|
|
onChange={(e) => setDreamInput(e.target.value)}
|
|
className="w-full h-32 bg-transparent text-white placeholder-purple-200/50 text-xl font-medium outline-none resize-none text-center"
|
|
placeholder={t("Describe your dream here... (e.g. I saw a snake in a temple)", "Describe your dream here... (e.g. I saw a snake in a temple)")}
|
|
aria-label="Describe your dream"
|
|
/>
|
|
<div className="flex justify-center mt-6">
|
|
<Button
|
|
onClick={handleDreamInterpret}
|
|
disabled={!dreamInput.trim() || isDreaming}
|
|
className="bg-white text-purple-900 hover:bg-purple-100 font-black px-12 py-6 rounded-[2rem] text-xl shadow-lg shadow-purple-500/30 transition-all hover:scale-105 active:scale-95 flex items-center gap-3"
|
|
>
|
|
{isDreaming ? <Loader2 className="animate-spin"/> : <Sparkles className="fill-purple-600"/>}
|
|
{t("REVEAL MEANING", "REVEAL MEANING")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{dreamResult && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in slide-in-from-bottom-8 duration-700">
|
|
<div className="bg-amber-900/40 backdrop-blur-md rounded-[2.5rem] p-8 border border-amber-500/30 hover:border-amber-500/60 transition-colors group">
|
|
<h3 className="text-amber-400 font-black uppercase tracking-widest text-xs mb-4 flex items-center gap-2">
|
|
<BookOpen size={16}/> {t("Traditional Folklore", "Traditional Folklore")}
|
|
</h3>
|
|
<p className="text-amber-100 font-medium text-lg leading-relaxed italic">
|
|
"{language === 'ne' ? dreamResult.folklore.ne : dreamResult.folklore.en}"
|
|
</p>
|
|
</div>
|
|
<div className="bg-cyan-900/40 backdrop-blur-md rounded-[2.5rem] p-8 border border-cyan-500/30 hover:border-cyan-500/60 transition-colors group">
|
|
<h3 className="text-cyan-400 font-black uppercase tracking-widest text-xs mb-4 flex items-center gap-2">
|
|
<Brain size={16}/> {t("Psychological View", "Psychological View")}
|
|
</h3>
|
|
<p className="text-cyan-100 font-medium text-lg leading-relaxed italic">
|
|
"{language === 'ne' ? dreamResult.psychology.ne : dreamResult.psychology.en}"
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* PERSONAL HEALTH VIEW */}
|
|
{activeTab === 'personal' && (
|
|
<div className="animate-in fade-in slide-in-from-right-8 duration-700">
|
|
{loadingLog ? (
|
|
<div className="flex flex-col items-center justify-center py-32 gap-4">
|
|
<Loader2 className="animate-spin text-red-600 w-12 h-12" />
|
|
<p className="text-gray-500 font-bold animate-pulse uppercase tracking-widest text-xs">Accessing Ritual Logs...</p>
|
|
</div>
|
|
) : log ? (
|
|
<div className="space-y-10">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
<div className="bg-white dark:bg-gray-800 p-10 rounded-[3rem] shadow-2xl border-2 border-blue-50 dark:border-blue-900/30 flex flex-col items-center group relative overflow-visible">
|
|
<div className="relative mb-8 pt-4">
|
|
{isWaterOverflowing && (
|
|
<>
|
|
<div className="absolute top-4 -left-2 w-1.5 h-20 bg-blue-400/40 rounded-full blur-[1px] animate-[slideDown_1.5s_infinite] origin-top"></div>
|
|
<div className="absolute top-4 -right-2 w-1.5 h-24 bg-blue-400/40 rounded-full blur-[1px] animate-[slideDown_1.8s_infinite] origin-top"></div>
|
|
</>
|
|
)}
|
|
<div className={`w-28 h-40 relative rounded-b-[2rem] border-x-4 border-b-8 border-blue-100/30 dark:border-blue-900/40 overflow-hidden bg-blue-50/10 dark:bg-gray-950 shadow-2xl transition-transform duration-500 group-hover:scale-105 z-10 ${isWaterOverflowing ? 'ring-2 ring-blue-400/20' : ''}`}>
|
|
<div className="absolute top-0 left-0 right-0 h-2 bg-white/20 border-b border-white/10 z-20"></div>
|
|
<div
|
|
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t from-blue-600 to-blue-400 transition-all duration-1000 ease-out`}
|
|
style={{ height: `${Math.min(100, (log.waterGlasses / 8) * 100)}%` }}
|
|
>
|
|
<div className={`absolute top-0 left-0 w-[200%] h-4 bg-white/30 animate-wave ${isWaterOverflowing ? 'opacity-90 scale-y-125' : 'opacity-40'}`}></div>
|
|
{isWaterOverflowing && <div className="absolute top-0 left-0 w-full h-full bg-blue-300/10 animate-pulse"></div>}
|
|
</div>
|
|
<div className="absolute top-0 left-2 w-3 h-full bg-white/10 blur-[2px] rounded-full pointer-events-none z-30"></div>
|
|
</div>
|
|
</div>
|
|
<h2 className="font-black text-gray-900 dark:text-white mb-6 text-xl uppercase tracking-tighter relative z-10">{t("Hydration Track", "Hydration Track")}</h2>
|
|
<div className="flex items-center gap-8 relative z-10">
|
|
<button onClick={() => updateLog({ ...log, waterGlasses: Math.max(0, log.waterGlasses - 1) })} className="w-14 h-14 rounded-2xl bg-gray-100 dark:bg-gray-700 hover:bg-red-50 dark:hover:bg-red-900/30 text-gray-500 hover:text-red-500 flex items-center justify-center transition-all active:scale-90 shadow-sm" aria-label="Decrease water"><Minus size={28}/></button>
|
|
<div className="text-center">
|
|
<span className={`text-7xl font-black drop-shadow-sm transition-colors ${isWaterOverflowing ? 'text-blue-500' : 'text-blue-600 dark:text-blue-400'}`}>{log.waterGlasses}</span>
|
|
<p className="text-[10px] text-gray-400 font-black uppercase tracking-[0.2em] mt-2">Glasses / 8</p>
|
|
</div>
|
|
<button onClick={() => updateLog({ ...log, waterGlasses: log.waterGlasses + 1 })} className="w-14 h-14 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center shadow-2xl active:scale-90 transition-all" aria-label="Increase water"><Plus size={28}/></button>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 p-10 rounded-[3rem] shadow-2xl border-2 border-yellow-50 dark:border-yellow-900/30 flex flex-col items-center">
|
|
<div className={`w-24 h-24 ${getMoodEmoji(log.mood).color} rounded-[2rem] flex items-center justify-center mb-8 shadow-xl rotate-3 animate-float text-5xl transition-all duration-500`} aria-hidden="true">{getMoodEmoji(log.mood).emoji}</div>
|
|
<h2 className="font-black text-gray-900 dark:text-white mb-8 text-xl uppercase tracking-tighter">{t("Mood Ritual", "Mood Ritual")}</h2>
|
|
<div className="grid grid-cols-2 gap-4 w-full">
|
|
{['Happy', 'Neutral', 'Stressed', 'Tired'].map((m: any) => (
|
|
<button key={m} onClick={() => updateLog({...log, mood: m})} className={`py-4 rounded-2xl text-sm font-black uppercase tracking-widest transition-all ${log.mood === m ? 'bg-yellow-500 text-white shadow-xl scale-105' : 'bg-gray-50 dark:bg-gray-700 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600'}`}>{t(m, m)}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 p-10 rounded-[3rem] shadow-2xl border-2 border-indigo-50 dark:border-indigo-900/30 flex flex-col items-center">
|
|
<div className="w-24 h-24 bg-indigo-100 dark:bg-indigo-900/40 rounded-full flex items-center justify-center text-indigo-600 mb-8 shadow-xl border-4 border-indigo-200 dark:border-indigo-800"><Moon size={48} /></div>
|
|
<h2 className="font-black text-gray-900 dark:text-white mb-6 text-xl uppercase tracking-tighter">{t("Recovery Sleep", "Recovery Sleep")}</h2>
|
|
<div className="flex items-center gap-8">
|
|
<button onClick={() => updateLog({ ...log, sleepHours: Math.max(0, log.sleepHours - 1) })} className="w-14 h-14 rounded-2xl bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 flex items-center justify-center active:scale-90 transition-all shadow-sm" aria-label="Decrease sleep"><Minus size={28}/></button>
|
|
<div className="text-center">
|
|
<span className="text-7xl font-black text-indigo-600 dark:text-indigo-400 drop-shadow-sm">{log.sleepHours}</span>
|
|
<p className="text-[10px] text-gray-400 font-black uppercase tracking-[0.2em] mt-2">Hours / 8</p>
|
|
</div>
|
|
<button onClick={() => updateLog({ ...log, sleepHours: log.sleepHours + 1 })} className="w-14 h-14 rounded-2xl bg-indigo-600 text-white flex items-center justify-center shadow-2xl active:scale-90 transition-all" aria-label="Increase sleep"><Plus size={28}/></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-32 opacity-40">
|
|
<BatteryMedium size={48} className="mx-auto mb-4" />
|
|
<p className="font-bold">Daily log failed to initialize.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* YOGA & EXERCISE VIEW */}
|
|
{activeTab === 'yog' && (
|
|
<div className="animate-in fade-in slide-up duration-700 space-y-12">
|
|
<div className="bg-gradient-to-r from-indigo-700 via-indigo-600 to-blue-800 rounded-[3.5rem] p-12 text-white shadow-2xl relative overflow-hidden group">
|
|
<div className="relative z-10 max-w-2xl space-y-6">
|
|
<h2 className="text-5xl font-black italic mb-2 uppercase tracking-tighter">{t("Yog Flow & Vitality", "Yog Flow & Vitality")}</h2>
|
|
<p className="text-indigo-100 text-xl font-medium leading-relaxed opacity-90">
|
|
{t("Connect with your inner self through traditional Nepali asanas. Balance the elements within.", "Connect with your inner self through traditional Nepali asanas. Balance the elements within.")}
|
|
</p>
|
|
<button onClick={handleStartFlow} className="px-10 py-5 bg-white text-indigo-900 rounded-[1.5rem] font-black text-lg shadow-2xl transform hover:scale-105 active:scale-95 transition-all flex items-center gap-4 group">
|
|
<Play size={24} className="fill-indigo-900 group-hover:scale-110 transition-transform"/> {t("Start Flow", "Start Flow")}
|
|
</button>
|
|
</div>
|
|
<Waves className="absolute -right-20 -bottom-20 text-white/5 w-[500px] h-[500px] rotate-12 group-hover:rotate-45 transition-transform duration-1000" />
|
|
</div>
|
|
|
|
{isSelectingPose && (
|
|
<div className="text-center animate-in fade-in slide-in-from-top-4 mb-4">
|
|
<p className="text-indigo-600 dark:text-indigo-400 font-black uppercase tracking-widest text-lg">Select a pose to begin session</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{YOGA_POSES.map(pose => (
|
|
<div
|
|
key={pose.id}
|
|
onClick={() => isSelectingPose ? handleSelectPose(pose) : null}
|
|
className={`bg-white dark:bg-gray-800 p-10 rounded-[3rem] shadow-sm border-2 border-gray-50 dark:border-gray-700 hover:shadow-2xl transition-all group cursor-pointer hover:-translate-y-2 ${isSelectingPose ? 'ring-4 ring-indigo-500/30 scale-105' : ''}`}
|
|
>
|
|
<div className="w-20 h-20 bg-indigo-50 dark:bg-indigo-900/30 rounded-[2rem] flex items-center justify-center text-indigo-600 mb-8 group-hover:scale-110 group-hover:rotate-12 transition-all shadow-xl">
|
|
<pose.icon size={44} />
|
|
</div>
|
|
<h3 className="text-3xl font-black text-gray-900 dark:text-white tracking-tighter mb-6">{language === 'en' ? pose.name : pose.neName}</h3>
|
|
<div className="space-y-6">
|
|
<div className="bg-gray-50 dark:bg-gray-900/50 p-4 rounded-2xl border-l-4 border-indigo-500">
|
|
<p className="text-[10px] uppercase font-black text-indigo-500 tracking-[0.3em] mb-2">{t("Core Benefit", "Core Benefit")}</p>
|
|
<p className="text-sm font-bold text-gray-700 dark:text-gray-300 leading-snug italic">{language === 'en' ? pose.benefits : pose.neBenefits}</p>
|
|
</div>
|
|
<div className="px-4">
|
|
<p className="text-[10px] uppercase font-black text-gray-400 tracking-[0.3em] mb-2">{t("Movement Guide", "Movement Guide")}</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium">{language === 'en' ? pose.steps : pose.neSteps}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ANCIENT WISDOM & LONGEVITY VIEW */}
|
|
{activeTab === 'ancient' && (
|
|
<div className="animate-in fade-in slide-in-from-left-8 duration-700 space-y-12">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
|
<div className="space-y-8">
|
|
<h2 className="text-3xl font-black text-amber-700 dark:text-amber-50 flex items-center gap-4 uppercase tracking-tighter italic">
|
|
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-2xl"><Sprout size={32}/></div> {t("Ayurvedic Rituals", "Ayurvedic Rituals")}
|
|
</h2>
|
|
<div className="space-y-6">
|
|
{AYURVEDA_TIPS.map((item, idx) => (
|
|
<div key={idx} className="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border-2 border-amber-50 dark:border-gray-700 flex gap-8 items-start hover:shadow-xl hover:border-amber-200 transition-all group">
|
|
<div className="p-5 bg-amber-50 dark:bg-amber-900/20 rounded-[1.5rem] shadow-xl text-amber-600 shrink-0 group-hover:scale-110 transition-transform">
|
|
<item.icon size={32}/>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-black text-amber-950 dark:text-amber-100 text-2xl uppercase tracking-tighter mb-2">{language === 'en' ? item.title : item.neTitle}</h3>
|
|
<p className="text-lg text-amber-800/80 dark:text-gray-400 leading-relaxed font-medium italic">
|
|
"{language === 'en' ? item.tip : item.neTip}"
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-8">
|
|
<h2 className="text-3xl font-black text-emerald-700 dark:text-emerald-50 flex items-center gap-4 uppercase tracking-tighter italic">
|
|
<div className="p-3 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl"><ShieldCheck size={32}/></div> {t("Longevity Secrets", "Longevity Secrets")}
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
{LONGEVITY_PILLARS.map((p, idx) => (
|
|
<div key={idx} className="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border-2 border-emerald-50 dark:border-gray-700 flex flex-col items-center text-center group hover:shadow-xl transition-all">
|
|
<div className="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/40 rounded-3xl flex items-center justify-center text-emerald-600 mb-6 group-hover:rotate-12 transition-transform shadow-lg">
|
|
<p.icon size={32}/>
|
|
</div>
|
|
<h3 className="text-xl font-black text-gray-900 dark:text-white mb-3 uppercase tracking-tighter">{language === 'en' ? p.en : p.ne}</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium">{language === 'en' ? p.desc : p.neDesc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<style>{`
|
|
@keyframes slideDown {
|
|
0% { transform: scaleY(0); opacity: 0.8; top: 1rem; }
|
|
50% { opacity: 0.6; }
|
|
100% { transform: scaleY(1); opacity: 0; top: 11rem; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
export default Health;
|