diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a350ef..314fa51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -238,3 +238,9 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). - Otimização do carregamento de dados com agrupamento eficiente - Integração com o tema existente do sistema - Sistema de filtragem temporal com conversão de datas +- Componente MetricsChart extraído e modularizado + - Interfaces e tipos bem definidos + - Lógica de filtragem encapsulada + - Estado interno gerenciado + - Props minimalistas e bem tipadas + - Componente reutilizável em outros contextos diff --git a/src/components/dashboard/MetricsChart.tsx b/src/components/dashboard/MetricsChart.tsx new file mode 100644 index 0000000..14755cc --- /dev/null +++ b/src/components/dashboard/MetricsChart.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import { Calendar, HelpCircle } from 'lucide-react'; +import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts'; + +interface WeeklyMetrics { + week: string; + fluency: number; + pronunciation: number; + accuracy: number; + comprehension: number; + wordsPerMinute: number; + pauses: number; + errors: number; + minutesRead: number; +} + +interface MetricConfig { + key: string; + name: string; + color: string; +} + +type TimeFilter = '3m' | '6m' | '12m' | 'all'; + +interface TimeFilterOption { + value: TimeFilter; + label: string; + months: number | null; +} + +const METRICS_CONFIG: MetricConfig[] = [ + { key: 'fluency', name: 'Fluência', color: '#6366f1' }, + { key: 'pronunciation', name: 'Pronúncia', color: '#f43f5e' }, + { key: 'accuracy', name: 'Precisão', color: '#0ea5e9' }, + { key: 'comprehension', name: 'Compreensão', color: '#10b981' }, + { key: 'wordsPerMinute', name: 'Palavras/Min', color: '#8b5cf6' } +]; + +const TIME_FILTERS: TimeFilterOption[] = [ + { value: '3m', label: '3 meses', months: 3 }, + { value: '6m', label: '6 meses', months: 6 }, + { value: '12m', label: '12 meses', months: 12 }, + { value: 'all', label: 'Todo período', months: null }, +]; + +interface MetricsChartProps { + data: WeeklyMetrics[]; + className?: string; +} + +export function MetricsChart({ data, className = '' }: MetricsChartProps) { + const [visibleMetrics, setVisibleMetrics] = React.useState>( + new Set(METRICS_CONFIG.map(metric => metric.key)) + ); + const [timeFilter, setTimeFilter] = React.useState('12m'); + + const toggleMetric = (metricKey: string) => { + setVisibleMetrics(prev => { + const newSet = new Set(prev); + if (newSet.has(metricKey)) { + newSet.delete(metricKey); + } else { + newSet.add(metricKey); + } + return newSet; + }); + }; + + const filterDataByTime = (data: WeeklyMetrics[]): WeeklyMetrics[] => { + if (timeFilter === 'all') return data; + + const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12; + const cutoffDate = new Date(); + cutoffDate.setMonth(cutoffDate.getMonth() - months); + + return data.filter(item => { + const [year, week] = item.week.split('-W').map(Number); + const itemDate = new Date(year, 0, 1 + (week - 1) * 7); + return itemDate >= cutoffDate; + }); + }; + + const filteredData = filterDataByTime(data); + + return ( +
+
+
+
+

Evolução das Métricas por Semana

+

Acompanhe seu progresso ao longo do tempo

+
+
+ {/* Filtro de Período */} +
+ + {TIME_FILTERS.map(filter => ( + + ))} +
+
+ +
+
+
+ + {/* Pill Buttons */} +
+ {METRICS_CONFIG.map(metric => ( + + ))} +
+ +
+ + + + + + + + + + + + + { + const metricNames: { [key: string]: string } = { + fluency: 'Fluência', + pronunciation: 'Pronúncia', + accuracy: 'Precisão', + comprehension: 'Compreensão', + wordsPerMinute: 'Palavras/Min', + pauses: 'Pausas', + errors: 'Erros', + minutesRead: 'Minutos Lidos' + }; + return [value, metricNames[name] || name]; + }} + contentStyle={{ + backgroundColor: 'rgba(255, 255, 255, 0.98)', + border: 'none', + borderRadius: '8px', + boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + padding: '12px' + }} + isAnimationActive={false} + /> + + {METRICS_CONFIG.map(metric => ( + visibleMetrics.has(metric.key) && ( + + ) + ))} + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/student-dashboard/StudentDashboardPage.tsx b/src/pages/student-dashboard/StudentDashboardPage.tsx index b99db8b..ba64934 100644 --- a/src/pages/student-dashboard/StudentDashboardPage.tsx +++ b/src/pages/student-dashboard/StudentDashboardPage.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Plus, BookOpen, Clock, TrendingUp, Award, Mic, Target, Brain, Gauge, Pause, XCircle, HelpCircle, Calendar } from 'lucide-react'; +import { Plus, BookOpen, Clock, TrendingUp, Award, Mic, Target, Brain, Gauge, Pause, XCircle, HelpCircle } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { supabase } from '../../lib/supabase'; import type { Story, Student } from '../../types/database'; -import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts'; +import { MetricsChart } from '@/components/dashboard/MetricsChart'; interface DashboardMetrics { totalStories: number; @@ -30,18 +30,6 @@ interface WeeklyMetrics { minutesRead: number; } -interface WeeklyData { - count: number; - fluency: number; - pronunciation: number; - accuracy: number; - comprehension: number; - wordsPerMinute: number; - pauses: number; - errors: number; - minutesRead: number; -} - interface Recording { created_at: string; fluency_score: number; @@ -53,35 +41,18 @@ interface Recording { error_count: number; } -interface MetricConfig { - key: string; - name: string; - color: string; +interface WeeklyData { + count: number; + fluency: number; + pronunciation: number; + accuracy: number; + comprehension: number; + wordsPerMinute: number; + pauses: number; + errors: number; + minutesRead: number; } -type TimeFilter = '3m' | '6m' | '12m' | 'all'; - -interface TimeFilterOption { - value: TimeFilter; - label: string; - months: number | null; -} - -const METRICS_CONFIG: MetricConfig[] = [ - { key: 'fluency', name: 'Fluência', color: '#6366f1' }, - { key: 'pronunciation', name: 'Pronúncia', color: '#f43f5e' }, - { key: 'accuracy', name: 'Precisão', color: '#0ea5e9' }, - { key: 'comprehension', name: 'Compreensão', color: '#10b981' }, - { key: 'wordsPerMinute', name: 'Palavras/Min', color: '#8b5cf6' } -]; - -const TIME_FILTERS: TimeFilterOption[] = [ - { value: '3m', label: '3 meses', months: 3 }, - { value: '6m', label: '6 meses', months: 6 }, - { value: '12m', label: '12 meses', months: 12 }, - { value: 'all', label: 'Todo período', months: null }, -]; - export function StudentDashboardPage() { const navigate = useNavigate(); const [student, setStudent] = React.useState(null); @@ -101,10 +72,6 @@ export function StudentDashboardPage() { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [recentStories, setRecentStories] = React.useState([]); - const [visibleMetrics, setVisibleMetrics] = React.useState>( - new Set(METRICS_CONFIG.map(metric => metric.key)) - ); - const [timeFilter, setTimeFilter] = React.useState('12m'); const processWeeklyMetrics = (recordings: Recording[]) => { const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => { @@ -153,32 +120,6 @@ export function StudentDashboardPage() { .sort((a, b) => a.week.localeCompare(b.week)); }; - const toggleMetric = (metricKey: string) => { - setVisibleMetrics(prev => { - const newSet = new Set(prev); - if (newSet.has(metricKey)) { - newSet.delete(metricKey); - } else { - newSet.add(metricKey); - } - return newSet; - }); - }; - - const filterDataByTime = (data: WeeklyMetrics[]): WeeklyMetrics[] => { - if (timeFilter === 'all') return data; - - const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12; - const cutoffDate = new Date(); - cutoffDate.setMonth(cutoffDate.getMonth() - months); - - return data.filter(item => { - const [year, week] = item.week.split('-W').map(Number); - const itemDate = new Date(year, 0, 1 + (week - 1) * 7); - return itemDate >= cutoffDate; - }); - }; - React.useEffect(() => { const fetchDashboardData = async () => { try { @@ -283,10 +224,10 @@ export function StudentDashboardPage() { // Calcular médias setMetrics({ - totalStories: allStoriesData.length, // Usando o total de histórias + totalStories: allStoriesData.length, averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings), - totalReadingTime: recordings.length * 2, // Exemplo: 2 minutos por gravação - currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)), // Exemplo: nível baseado na fluência + totalReadingTime: recordings.length * 2, + currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)), averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings), averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings), averageComprehension: Math.round(metricsSum.comprehension / totalRecordings), @@ -339,174 +280,6 @@ export function StudentDashboardPage() { ); } - const renderMetricsChart = () => { - const filteredData = filterDataByTime(weeklyMetrics); - - return ( -
-
-
-
-

Evolução das Métricas por Semana

-

Acompanhe seu progresso ao longo do tempo

-
-
- {/* Filtro de Período */} -
- - {TIME_FILTERS.map(filter => ( - - ))} -
-
- -
-
-
- - {/* Pill Buttons */} -
- {METRICS_CONFIG.map(metric => ( - - ))} -
- -
- - - - - - - - - - - - - { - const metricNames: { [key: string]: string } = { - fluency: 'Fluência', - pronunciation: 'Pronúncia', - accuracy: 'Precisão', - comprehension: 'Compreensão', - wordsPerMinute: 'Palavras/Min', - pauses: 'Pausas', - errors: 'Erros', - minutesRead: 'Minutos Lidos' - }; - return [value, metricNames[name] || name]; - }} - contentStyle={{ - backgroundColor: 'rgba(255, 255, 255, 0.98)', - border: 'none', - borderRadius: '8px', - boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', - padding: '12px' - }} - isAnimationActive={false} - /> - - {METRICS_CONFIG.map(metric => ( - visibleMetrics.has(metric.key) && ( - - ) - ))} - - - -
-
-
- ); - }; - return (
{/* Cabeçalho */} @@ -736,7 +509,7 @@ export function StudentDashboardPage() {
{/* Gráfico de Evolução */} - {renderMetricsChart()} + {/* Histórias Recentes */}