/
klsfv
/
CalorieTracker
Обзор
Документация
Войти
/
klsfv
/
CalorieTracker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/StatisticsScreen.tsx
418 строк
19 KB
klsfv
upload files
15 янв 2026, 03:19
15 янв 2026, 03:19
09dece5
Код
Авторство
О чём код?
import React, { useState, useEffect } from 'react'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { ArrowLeft, TrendingUp, TrendingDown, Award, Flame, Calendar, LogOut, Loader2 } from 'lucide-react'; import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BackendService } from '../services/backend.service'; import type { WeeklyStatistics } from '../types'; interface StatisticsScreenProps { onNavigate: (screen: string) => void; onLogout: () => void; } // Названия дней недели const dayNames = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']; // Функция для получения названия дня недели function getDayName(dateString: string): string { const date = new Date(dateString); return dayNames[date.getDay()]; } // Функция для форматирования даты function formatDate(dateString: string): string { const date = new Date(dateString); const dayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']; return dayNames[date.getDay()]; } export function StatisticsScreen({ onNavigate, onLogout }: StatisticsScreenProps) { const [weeklyStats, setWeeklyStats] = useState<WeeklyStatistics | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); // Загрузка данных при монтировании компонента useEffect(() => { loadWeeklyStatistics(); }, []); const loadWeeklyStatistics = async () => { try { setIsLoading(true); setError(null); // Получаем данные за последние 7 дней const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 6); const stats = await BackendService.statistics.getWeekly( startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0] ); setWeeklyStats(stats); } catch (err: any) { console.error('Error loading statistics:', err); setError(err.message || 'Не удалось загрузить статистику'); } finally { setIsLoading(false); } }; // Преобразование данных для графиков const weekData = weeklyStats?.dailyStats.map(day => ({ day: getDayName(day.date), calories: day.totalCalories || 0, protein: Math.round(day.totalProtein || 0), fat: Math.round(day.totalFat || 0), carbs: Math.round(day.totalCarbs || 0), })) || []; const avgCalories = Math.round(weeklyStats?.averageCalories || 0); const avgProtein = Math.round(weeklyStats?.averageProtein || 0); const avgFat = Math.round(weeklyStats?.averageFat || 0); const avgCarbs = Math.round(weeklyStats?.averageCarbs || 0); const totalCalories = weeklyStats?.totalCalories || 0; const weekGoal = 14000; // 7 дней * 2000 ккал const weekProgress = totalCalories > 0 ? ((totalCalories / weekGoal) * 100).toFixed(0) : '0'; // Лучший день const bestDay = weeklyStats?.bestDay; const bestDayName = bestDay ? formatDate(bestDay.date) : 'Нет данных'; const bestDayCalories = bestDay?.calories || 0; // Стабильность (adherence percentage) const adherencePercentage = Math.round(weeklyStats?.adherencePercentage || 0); // Состояние загрузки if (isLoading) { return ( <div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-zinc-800 relative overflow-hidden flex items-center justify-center"> <div className="text-center"> <Loader2 className="w-12 h-12 text-white animate-spin mx-auto mb-4" /> <p className="text-white text-lg">Загрузка статистики...</p> </div> </div> ); } // Состояние ошибки if (error) { return ( <div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-zinc-800 relative overflow-hidden"> <div className="max-w-6xl mx-auto p-4"> <Card className="bg-red-900/20 border-red-500/50"> <CardContent className="pt-6"> <div className="text-center"> <p className="text-red-400 text-lg mb-4">{error}</p> <Button onClick={loadWeeklyStatistics} variant="outline"> Попробовать снова </Button> </div> </CardContent> </Card> </div> </div> ); } // Если нет данных if (!weeklyStats || weekData.length === 0) { return ( <div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-zinc-800 relative overflow-hidden"> <div className="max-w-6xl mx-auto p-4"> <Card className="bg-zinc-900/80 border-zinc-800"> <CardContent className="pt-6"> <div className="text-center"> <Calendar className="w-16 h-16 text-zinc-600 mx-auto mb-4" /> <p className="text-zinc-400 text-lg mb-2">Нет данных за неделю</p> <p className="text-zinc-500 text-sm">Добавьте записи питания, чтобы увидеть статистику</p> </div> </CardContent> </Card> </div> </div> ); } return ( <div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-zinc-800 relative overflow-hidden"> {/* Декоративные элементы */} <div className="absolute top-0 right-0 w-96 h-96 bg-white/5 rounded-full blur-3xl"></div> <div className="absolute bottom-20 left-0 w-96 h-96 bg-white/5 rounded-full blur-3xl"></div> {/* Header */} <div className="bg-zinc-900/80 backdrop-blur-xl border-b border-zinc-800 sticky top-0 z-10 shadow-lg"> <div className="max-w-6xl mx-auto p-4"> <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> <Button variant="ghost" size="icon" onClick={() => onNavigate('diary')} className="text-zinc-400 hover:text-white hover:bg-zinc-800 hover:border-white/30 border border-transparent transition-all" > <ArrowLeft className="w-5 h-5" /> </Button> <div className="flex items-center gap-3"> <div className="w-10 h-10 bg-gradient-to-br from-white/10 to-zinc-100/10 rounded-xl flex items-center justify-center border border-white/20"> <TrendingUp className="w-5 h-5 text-white" /> </div> <div> <h1 className="text-white">Статистика и аналитика</h1> <p className="text-sm text-zinc-400">Ваш прогресс за неделю</p> </div> </div> </div> <div className="flex items-center gap-2"> <Badge className="bg-gradient-to-r from-white via-zinc-100 to-zinc-200 text-zinc-900 border-0 shadow-lg"> <Award className="w-3 h-3 mr-1" /> {weekProgress}% недели </Badge> <Button variant="ghost" size="sm" onClick={onLogout} className="text-zinc-400 hover:text-white hover:bg-zinc-800 hover:border-red-500/30 border border-transparent transition-all" > <LogOut className="w-4 h-4 mr-2" /> Выход </Button> </div> </div> </div> </div> <div className="max-w-6xl mx-auto p-4 space-y-4 relative z-10"> {/* Weekly Summary */} <Card className="bg-gradient-to-br from-white/10 via-zinc-800/80 to-zinc-900/80 border-white/20 shadow-xl shadow-white/10 overflow-hidden relative backdrop-blur"> <div className="absolute top-0 right-0 w-40 h-40 bg-white/10 rounded-full blur-2xl"></div> <CardHeader> <div className="flex items-center justify-between"> <CardTitle className="text-white flex items-center gap-2"> <Calendar className="w-5 h-5 text-white" /> Итоги недели </CardTitle> <div className="flex items-center gap-2"> <Calendar className="w-4 h-4 text-white" /> <span className="text-sm text-zinc-400"> {weeklyStats.startDate} - {weeklyStats.endDate} </span> </div> </div> </CardHeader> <CardContent> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="text-center bg-zinc-900/70 backdrop-blur rounded-xl p-5 border border-white/20 shadow-lg relative overflow-hidden"> <div className="absolute top-0 right-0 w-20 h-20 bg-white/10 rounded-full blur-xl"></div> <Flame className="w-8 h-8 text-white mx-auto mb-3 relative z-10" /> <div className="text-4xl text-white relative z-10">{totalCalories.toLocaleString()}</div> <div className="text-xs text-zinc-400 mt-1 relative z-10">Всего калорий</div> <Badge className="mt-2 bg-white/10 text-white border-white/20 text-xs relative z-10"> 7 дней </Badge> </div> <div className="text-center bg-zinc-900/70 backdrop-blur rounded-xl p-5 border border-zinc-100/20 shadow-lg"> <div className="w-8 h-8 bg-zinc-100/20 rounded-full mx-auto mb-3 flex items-center justify-center"> <div className="w-4 h-4 bg-zinc-100 rounded-full"></div> </div> <div className="text-4xl text-zinc-100">{avgProtein}</div> <div className="text-xs text-zinc-400 mt-1">Белки/день (г)</div> <Badge className="mt-2 bg-zinc-100/10 text-zinc-100 border-zinc-100/20 text-xs"> Среднее </Badge> </div> <div className="text-center bg-zinc-900/70 backdrop-blur rounded-xl p-5 border border-yellow-500/30 shadow-lg"> <div className="w-8 h-8 bg-yellow-500/30 rounded-full mx-auto mb-3 flex items-center justify-center"> <div className="w-4 h-4 bg-yellow-400 rounded-full"></div> </div> <div className="text-4xl text-yellow-400">{avgFat}</div> <div className="text-xs text-zinc-400 mt-1">Жиры/день (г)</div> <Badge className="mt-2 bg-yellow-500/20 text-yellow-400 border-yellow-500/30 text-xs"> Среднее </Badge> </div> <div className="text-center bg-zinc-900/70 backdrop-blur rounded-xl p-5 border border-purple-500/30 shadow-lg"> <div className="w-8 h-8 bg-purple-500/30 rounded-full mx-auto mb-3 flex items-center justify-center"> <div className="w-4 h-4 bg-purple-400 rounded-full"></div> </div> <div className="text-4xl text-purple-400">{avgCarbs}</div> <div className="text-xs text-zinc-400 mt-1">Углеводы/день (г)</div> <Badge className="mt-2 bg-purple-500/20 text-purple-400 border-purple-500/30 text-xs"> Среднее </Badge> </div> </div> </CardContent> </Card> {/* Daily Average */} <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Card className="bg-zinc-900/80 backdrop-blur border-zinc-800 shadow-xl"> <CardContent className="pt-6"> <div className="flex items-center justify-between mb-2"> <span className="text-zinc-400 text-sm">Средние калории</span> <TrendingUp className="w-4 h-4 text-green-400" /> </div> <div className="text-3xl text-white">{avgCalories}</div> <div className="text-xs text-zinc-500 mt-1">ккал/день</div> <div className="mt-3 pt-3 border-t border-zinc-800 text-sm text-green-400"> {avgCalories > 2000 ? `+${((avgCalories - 2000) / 2000 * 100).toFixed(1)}% от цели` : `${((avgCalories / 2000) * 100).toFixed(1)}% от цели`} </div> </CardContent> </Card> <Card className="bg-zinc-900/80 backdrop-blur border-zinc-800 shadow-xl"> <CardContent className="pt-6"> <div className="flex items-center justify-between mb-2"> <span className="text-zinc-400 text-sm">Лучший день</span> <Award className="w-4 h-4 text-white" /> </div> <div className="text-3xl text-white">{bestDayName}</div> <div className="text-xs text-zinc-500 mt-1">{bestDayCalories} ккал</div> <div className="mt-3 pt-3 border-t border-zinc-800 text-sm text-white"> Отлично! </div> </CardContent> </Card> <Card className="bg-zinc-900/80 backdrop-blur border-zinc-800 shadow-xl"> <CardContent className="pt-6"> <div className="flex items-center justify-between mb-2"> <span className="text-zinc-400 text-sm">Стабильность</span> <TrendingDown className="w-4 h-4 text-blue-400" /> </div> <div className="text-3xl text-white">{adherencePercentage}%</div> <div className="text-xs text-zinc-500 mt-1">соблюдение режима</div> <div className="mt-3 pt-3 border-t border-zinc-800 text-sm text-blue-400"> {adherencePercentage >= 80 ? 'Отлично' : adherencePercentage >= 60 ? 'Хорошо' : 'Можно лучше'} </div> </CardContent> </Card> </div> {/* Calories Chart */} <Card className="bg-zinc-900/80 backdrop-blur border-zinc-800 shadow-xl overflow-hidden"> <CardHeader className="bg-gradient-to-r from-white/5 to-zinc-100/5 border-b border-zinc-800"> <div className="flex items-center justify-between"> <CardTitle className="text-white flex items-center gap-2"> <Flame className="w-5 h-5 text-white" /> График калорий </CardTitle> <Badge className="bg-white/10 text-white border-white/20"> 7 дней </Badge> </div> </CardHeader> <CardContent className="pt-6"> <ResponsiveContainer width="100%" height={320}> <LineChart data={weekData}> <defs> <linearGradient id="colorCalories" x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor="#ffffff" stopOpacity={0.3}/> <stop offset="95%" stopColor="#ffffff" stopOpacity={0}/> </linearGradient> </defs> <CartesianGrid strokeDasharray="3 3" stroke="#3f3f46" strokeOpacity={0.3} /> <XAxis dataKey="day" stroke="#a1a1aa" tick={{ fill: '#a1a1aa' }} tickLine={{ stroke: '#3f3f46' }} /> <YAxis stroke="#a1a1aa" tick={{ fill: '#a1a1aa' }} tickLine={{ stroke: '#3f3f46' }} /> <Tooltip contentStyle={{ backgroundColor: '#18181b', border: '1px solid #3f3f46', borderRadius: '12px', color: '#fff', boxShadow: '0 10px 40px rgba(255, 255, 255, 0.2)', }} labelStyle={{ color: '#ffffff' }} /> <Line type="monotone" dataKey="calories" stroke="#ffffff" strokeWidth={4} dot={{ fill: '#ffffff', r: 6, strokeWidth: 2, stroke: '#18181b' }} activeDot={{ r: 8, strokeWidth: 3, stroke: '#d4d4d8' }} fill="url(#colorCalories)" /> </LineChart> </ResponsiveContainer> </CardContent> </Card> {/* Macronutrients Chart */} <Card className="bg-zinc-900/80 backdrop-blur border-zinc-800 shadow-xl overflow-hidden"> <CardHeader className="bg-gradient-to-r from-white/5 to-zinc-100/5 border-b border-zinc-800"> <div className="flex items-center justify-between"> <CardTitle className="text-white flex items-center gap-2"> <TrendingUp className="w-5 h-5 text-white" /> Баланс макронутриентов </CardTitle> <div className="flex gap-2"> <Badge className="bg-zinc-100/10 text-zinc-100 border-zinc-100/20"> <span className="w-2 h-2 bg-zinc-100 rounded-full mr-1"></span> Белки </Badge> <Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30"> <span className="w-2 h-2 bg-yellow-400 rounded-full mr-1"></span> Жиры </Badge> <Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30"> <span className="w-2 h-2 bg-purple-400 rounded-full mr-1"></span> Углеводы </Badge> </div> </div> </CardHeader> <CardContent className="pt-6"> <ResponsiveContainer width="100%" height={320}> <BarChart data={weekData}> <CartesianGrid strokeDasharray="3 3" stroke="#3f3f46" strokeOpacity={0.3} /> <XAxis dataKey="day" stroke="#a1a1aa" tick={{ fill: '#a1a1aa' }} tickLine={{ stroke: '#3f3f46' }} /> <YAxis stroke="#a1a1aa" tick={{ fill: '#a1a1aa' }} tickLine={{ stroke: '#3f3f46' }} /> <Tooltip contentStyle={{ backgroundColor: '#18181b', border: '1px solid #3f3f46', borderRadius: '12px', color: '#fff', boxShadow: '0 10px 40px rgba(255, 255, 255, 0.2)', }} /> <Legend wrapperStyle={{ paddingTop: '20px' }} iconType="circle" /> <Bar dataKey="protein" fill="#d4d4d8" name="Белки (г)" radius={[8, 8, 0, 0]} /> <Bar dataKey="fat" fill="#eab308" name="Жиры (г)" radius={[8, 8, 0, 0]} /> <Bar dataKey="carbs" fill="#a855f7" name="Углеводы (г)" radius={[8, 8, 0, 0]} /> </BarChart> </ResponsiveContainer> </CardContent> </Card> </div> </div> ); }