Polish MoodTracker component with premium glassmorphism and UI improvements
This commit is contained in:
parent
c518ad9f34
commit
ce9cd0cce7
@ -3,10 +3,11 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Smile, Meh, Frown, TrendingUp, ChevronLeft, ChevronRight, MessageSquare, Quote } from 'lucide-react';
|
||||
import { Smile, Meh, Frown, TrendingUp, ChevronLeft, ChevronRight, MessageSquare, Quote, Sparkles } from 'lucide-react';
|
||||
import { MoodEntry, fetchMoodEntries, saveMoodEntry } from '@/lib/storage';
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Cell, ReferenceLine } from 'recharts';
|
||||
import { format, subDays, startOfWeek, endOfWeek, isWithinInterval, eachDayOfInterval, parseISO } from 'date-fns';
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, Tooltip, Cell } from 'recharts';
|
||||
import { format, subDays, startOfWeek, endOfWeek, eachDayOfInterval } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function MoodTracker() {
|
||||
const [entries, setEntries] = useState<MoodEntry[]>([]);
|
||||
@ -22,7 +23,12 @@ export function MoodTracker() {
|
||||
const data = await fetchMoodEntries();
|
||||
setEntries(data);
|
||||
if (data.length > 0) {
|
||||
setActiveMood(data[0].mood as any);
|
||||
// If the most recent entry is today, set it as active
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
const lastEntry = data[0];
|
||||
if (lastEntry.date === today) {
|
||||
setActiveMood(lastEntry.mood as any);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
@ -34,7 +40,6 @@ export function MoodTracker() {
|
||||
setAffirmation(null);
|
||||
setActiveMood(mood);
|
||||
|
||||
// Clear previous timeout if it exists
|
||||
if (currentTimeout) {
|
||||
clearTimeout(currentTimeout);
|
||||
setCurrentTimeout(null);
|
||||
@ -43,10 +48,13 @@ export function MoodTracker() {
|
||||
try {
|
||||
const result = await saveMoodEntry(mood);
|
||||
if (result) {
|
||||
setEntries(prev => [result.entry, ...prev]);
|
||||
setEntries(prev => {
|
||||
// Remove existing entry for today if it exists to avoid duplicates in state
|
||||
const filtered = prev.filter(e => e.date !== result.entry.date);
|
||||
return [result.entry, ...filtered];
|
||||
});
|
||||
setAffirmation(result.affirmation);
|
||||
|
||||
// Clear affirmation after 8 seconds
|
||||
const timeout = setTimeout(() => {
|
||||
setAffirmation(null);
|
||||
setCurrentTimeout(null);
|
||||
@ -66,25 +74,23 @@ export function MoodTracker() {
|
||||
|
||||
return days.map(day => {
|
||||
const dateStr = format(day, 'yyyy-MM-dd');
|
||||
const dayEntries = entries.filter(e => e.date === dateStr);
|
||||
// Find the *latest* entry for this day
|
||||
const dayEntry = entries.find(e => e.date === dateStr);
|
||||
|
||||
// Map mood to numeric values for graphing: bad=1, neutral=2, good=3
|
||||
// Map mood to numeric values: bad=1, neutral=2, good=3
|
||||
let value = 0;
|
||||
if (dayEntries.length > 0) {
|
||||
const total = dayEntries.reduce((sum, e) => {
|
||||
if (e.mood === 'good') return sum + 3;
|
||||
if (e.mood === 'neutral') return sum + 2;
|
||||
if (e.mood === 'bad') return sum + 1;
|
||||
return sum;
|
||||
}, 0);
|
||||
value = total / dayEntries.length;
|
||||
if (dayEntry) {
|
||||
if (dayEntry.mood === 'good') value = 3;
|
||||
else if (dayEntry.mood === 'neutral') value = 2;
|
||||
else if (dayEntry.mood === 'bad') value = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
name: format(day, 'EEE'),
|
||||
fullDate: dateStr,
|
||||
value: value === 0 ? null : Number(value.toFixed(1)),
|
||||
count: dayEntries.length
|
||||
value: value === 0 ? 0.2 : value, // 0.2 provides a small placeholder bar
|
||||
isPlaceholder: value === 0,
|
||||
mood: dayEntry?.mood
|
||||
};
|
||||
});
|
||||
}, [entries, weekOffset]);
|
||||
@ -97,145 +103,180 @@ export function MoodTracker() {
|
||||
return `${format(start, 'MMM d')} - ${format(end, 'MMM d')}`;
|
||||
}, [weekOffset]);
|
||||
|
||||
// Dynamic styles based on active mood
|
||||
const getMoodStyles = () => {
|
||||
const getGradient = () => {
|
||||
switch (activeMood) {
|
||||
case 'good':
|
||||
return 'from-emerald-600/20 via-teal-500/10 to-indigo-500/10 border-emerald-500/30 shadow-emerald-500/10';
|
||||
return 'from-emerald-500/10 via-teal-500/5 to-emerald-500/10 border-emerald-500/20 shadow-emerald-500/10';
|
||||
case 'neutral':
|
||||
return 'from-amber-600/20 via-orange-500/10 to-purple-500/10 border-amber-500/30 shadow-amber-500/10';
|
||||
return 'from-amber-500/10 via-orange-500/5 to-amber-500/10 border-amber-500/20 shadow-amber-500/10';
|
||||
case 'bad':
|
||||
return 'from-rose-600/20 via-pink-500/10 to-blue-500/10 border-rose-500/30 shadow-rose-500/10';
|
||||
return 'from-rose-500/10 via-red-500/5 to-rose-500/10 border-rose-500/20 shadow-rose-500/10';
|
||||
default:
|
||||
return 'from-indigo-600/20 via-purple-500/10 to-pink-500/10 border-white/10 shadow-indigo-500/10';
|
||||
return 'from-violet-500/10 via-indigo-500/5 to-violet-500/10 border-white/10 shadow-indigo-500/5';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`overflow-hidden border transition-all duration-1000 backdrop-blur-md shadow-2xl hover-lift bg-gradient-to-br ${getMoodStyles()}`}>
|
||||
<CardHeader className="pb-2 border-b border-white/5 bg-black/20">
|
||||
<Card className={cn(
|
||||
"overflow-hidden transition-all duration-700 backdrop-blur-xl border shadow-xl",
|
||||
"bg-gradient-to-br",
|
||||
getGradient()
|
||||
)}>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-bold flex items-center gap-2 text-white">
|
||||
<MessageSquare className="w-5 h-5 text-indigo-400" />
|
||||
How do you feel?
|
||||
<CardTitle className="text-base sm:text-lg font-medium flex items-center gap-2 text-white/90">
|
||||
<div className={cn("p-1.5 rounded-lg transition-colors duration-500",
|
||||
activeMood === 'good' ? "bg-emerald-500/20 text-emerald-400" :
|
||||
activeMood === 'neutral' ? "bg-amber-500/20 text-amber-400" :
|
||||
activeMood === 'bad' ? "bg-rose-500/20 text-rose-400" :
|
||||
"bg-white/10 text-white/70"
|
||||
)}>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
How are you feeling?
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1 bg-white/5 rounded-full p-1 border border-white/10">
|
||||
|
||||
<div className="flex items-center gap-1 bg-black/20 rounded-full p-0.5 border border-white/5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 rounded-full text-white/70 hover:text-white"
|
||||
className="h-6 w-6 rounded-full text-white/50 hover:text-white hover:bg-white/10"
|
||||
onClick={() => setWeekOffset(prev => prev + 1)}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
</Button>
|
||||
<span className="text-[10px] font-medium px-2 text-white/90 uppercase tracking-wider">{weekLabel}</span>
|
||||
<span className="text-[10px] font-medium px-2 text-white/60 uppercase tracking-wider min-w-[60px] text-center">
|
||||
{weekLabel}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 rounded-full text-white/70 hover:text-white disabled:opacity-30"
|
||||
className="h-6 w-6 rounded-full text-white/50 hover:text-white hover:bg-white/10 disabled:opacity-20"
|
||||
disabled={weekOffset === 0}
|
||||
onClick={() => setWeekOffset(prev => prev - 1)}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6 space-y-6">
|
||||
{/* Mood Buttons */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => handleMoodSelect('good')}
|
||||
disabled={isSaving}
|
||||
className="group flex flex-col items-center gap-2 p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 hover:bg-emerald-500/20 transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<div className="p-3 rounded-full bg-emerald-500/20 group-hover:bg-emerald-500/40 transition-colors">
|
||||
<Smile className="w-8 h-8 text-emerald-400 group-hover:scale-110 transition-transform" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-emerald-400 uppercase tracking-tighter">Good</span>
|
||||
</button>
|
||||
<CardContent className="pt-6 space-y-8">
|
||||
{/* Mood Selection */}
|
||||
<div className="grid grid-cols-3 gap-3 sm:gap-6">
|
||||
{[
|
||||
{ id: 'good', icon: Smile, label: 'Good', color: 'emerald' },
|
||||
{ id: 'neutral', icon: Meh, label: 'Okay', color: 'amber' },
|
||||
{ id: 'bad', icon: Frown, label: 'Bad', color: 'rose' }
|
||||
].map((item) => {
|
||||
const isSelected = activeMood === item.id;
|
||||
const Icon = item.icon;
|
||||
|
||||
<button
|
||||
onClick={() => handleMoodSelect('neutral')}
|
||||
disabled={isSaving}
|
||||
className="group flex flex-col items-center gap-2 p-4 rounded-2xl bg-amber-500/10 border border-amber-500/20 hover:bg-amber-500/20 transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<div className="p-3 rounded-full bg-amber-500/20 group-hover:bg-amber-500/40 transition-colors">
|
||||
<Meh className="w-8 h-8 text-amber-400 group-hover:rotate-12 transition-transform" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-amber-400 uppercase tracking-tighter">Neutral</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleMoodSelect('bad')}
|
||||
disabled={isSaving}
|
||||
className="group flex flex-col items-center gap-2 p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<div className="p-3 rounded-full bg-rose-500/20 group-hover:bg-rose-500/40 transition-colors">
|
||||
<Frown className="w-8 h-8 text-rose-400 group-hover:-translate-y-1 transition-transform" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-rose-400 uppercase tracking-tighter">Bad</span>
|
||||
</button>
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleMoodSelect(item.id as any)}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
"group relative flex flex-col items-center justify-center gap-3 p-4 rounded-2xl transition-all duration-300",
|
||||
"border",
|
||||
isSelected
|
||||
? `bg-${item.color}-500/20 border-${item.color}-500/50 shadow-[0_0_20px_-5px_var(--color-${item.color}-500)]`
|
||||
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 hover:-translate-y-1"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-3 rounded-xl transition-all duration-300",
|
||||
isSelected ? `bg-${item.color}-500 text-white shadow-lg scale-110` : `bg-white/5 text-white/60 group-hover:text-${item.color}-400 group-hover:scale-110`
|
||||
)}>
|
||||
<Icon className={cn("w-6 h-6", isSelected && "animate-pulse-subtle")} />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-xs font-semibold tracking-wide uppercase transition-colors",
|
||||
isSelected ? "text-white" : "text-white/40 group-hover:text-white/80"
|
||||
)}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Affirmation Message */}
|
||||
{affirmation && (
|
||||
<div className="animate-fade-in-up bg-white/5 border border-white/10 rounded-xl p-4 flex gap-3 items-start">
|
||||
<Quote className="w-4 h-4 text-indigo-400 flex-shrink-0 mt-1" />
|
||||
<p className="text-sm italic text-white/90 leading-relaxed">{affirmation}</p>
|
||||
{/* Dynamic Affirmation */}
|
||||
<div className={cn(
|
||||
"relative overflow-hidden transition-all duration-500 ease-out",
|
||||
affirmation ? "opacity-100 max-h-32 translate-y-0" : "opacity-0 max-h-0 translate-y-4"
|
||||
)}>
|
||||
<div className="bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/20 rounded-xl p-4 flex gap-4 items-center">
|
||||
<div className="p-2 bg-indigo-500/20 rounded-full shrink-0">
|
||||
<Quote className="w-4 h-4 text-indigo-400" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-indigo-100/90 leading-relaxed">
|
||||
{affirmation}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Week Graph */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold text-white/40 uppercase tracking-widest pl-1">
|
||||
{/* Mini Graph */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-white/40 uppercase tracking-widest px-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
Mood Trend
|
||||
<span>Mood Tracking</span>
|
||||
</div>
|
||||
<div className="h-32 w-full">
|
||||
|
||||
<div className="h-32 w-full bg-white/5 rounded-xl border border-white/5 p-4 relative group">
|
||||
{/* Grid lines background effect */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:14px_14px] mask-image-linear-gradient(to_bottom,transparent,black)] pointer-events-none" />
|
||||
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={weeklyData} margin={{ top: 5, right: 0, left: -20, bottom: 0 }}>
|
||||
<BarChart data={weeklyData} barSize={36}>
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)', radius: 4 }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
if (data.isPlaceholder) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900/90 border border-white/10 p-2 rounded-lg shadow-xl backdrop-blur-md">
|
||||
<p className="text-xs font-medium text-white mb-1">{data.fullDate}</p>
|
||||
<p className={cn(
|
||||
"text-xs font-bold capitalize",
|
||||
data.mood === 'good' ? "text-emerald-400" :
|
||||
data.mood === 'neutral' ? "text-amber-400" :
|
||||
"text-rose-400"
|
||||
)}>
|
||||
{data.mood}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10, dy: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 3]}
|
||||
ticks={[1, 2, 3]}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
color: '#fff'
|
||||
}}
|
||||
labelStyle={{ display: 'none' }}
|
||||
formatter={(value: number | undefined) => {
|
||||
if (value === undefined) return ['', ''];
|
||||
if (value >= 2.5) return ['Good', 'Mood'];
|
||||
if (value >= 1.5) return ['Neutral', 'Mood'];
|
||||
if (value > 0) return ['Bad', 'Mood'];
|
||||
return ['No Record', 'Mood'];
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[4, 4, 4, 4]} barSize={24}>
|
||||
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
|
||||
{weeklyData.map((entry, index) => {
|
||||
let color = 'rgba(255,255,255,0.1)';
|
||||
if (entry.value) {
|
||||
if (entry.value >= 2.5) color = '#10b981'; // emerald-500
|
||||
else if (entry.value >= 1.5) color = '#f59e0b'; // amber-500
|
||||
else color = '#f43f5e'; // rose-500
|
||||
let color = 'rgba(255,255,255,0.05)'; // Placeholder color
|
||||
|
||||
if (!entry.isPlaceholder) {
|
||||
if (entry.mood === 'good') color = '#10b981'; // emerald-500
|
||||
else if (entry.mood === 'neutral') color = '#f59e0b'; // amber-500
|
||||
else if (entry.mood === 'bad') color = '#f43f5e'; // rose-500
|
||||
}
|
||||
return <Cell key={`cell-${index}`} fill={color} />;
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={color}
|
||||
className="transition-all duration-300 hover:opacity-80"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user