mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 06:17:56 +00:00
refactor: extrai componente MetricsChart do dashboard
Modulariza o gráfico de métricas em um componente reutilizável: - Cria novo componente @/components/dashboard/MetricsChart - Move toda a lógica de filtragem e visualização para o componente - Define interfaces e tipos apropriados - Encapsula estados e lógica de filtragem - Simplifica a interface do componente (props) - Mantém toda funcionalidade existente Mudanças técnicas: - Extrai interfaces e tipos relacionados - Move constantes de configuração para o componente - Encapsula lógica de filtragem temporal - Simplifica o StudentDashboardPage - Melhora a manutenibilidade e reusabilidade
This commit is contained in:
parent
7bb2a9a1b7
commit
7a0bc3f8ca
@ -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
|
- Otimização do carregamento de dados com agrupamento eficiente
|
||||||
- Integração com o tema existente do sistema
|
- Integração com o tema existente do sistema
|
||||||
- Sistema de filtragem temporal com conversão de datas
|
- 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
|
||||||
|
|||||||
248
src/components/dashboard/MetricsChart.tsx
Normal file
248
src/components/dashboard/MetricsChart.tsx
Normal file
@ -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<Set<string>>(
|
||||||
|
new Set(METRICS_CONFIG.map(metric => metric.key))
|
||||||
|
);
|
||||||
|
const [timeFilter, setTimeFilter] = React.useState<TimeFilter>('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 (
|
||||||
|
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">Evolução das Métricas por Semana</h2>
|
||||||
|
<p className="text-sm text-gray-500">Acompanhe seu progresso ao longo do tempo</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Filtro de Período */}
|
||||||
|
<div className="flex items-center gap-2 bg-gray-50 p-1 rounded-lg">
|
||||||
|
<Calendar className="h-4 w-4 text-gray-500" />
|
||||||
|
{TIME_FILTERS.map(filter => (
|
||||||
|
<button
|
||||||
|
key={filter.value}
|
||||||
|
onClick={() => setTimeFilter(filter.value)}
|
||||||
|
className={`
|
||||||
|
px-3 py-1 rounded-md text-sm font-medium transition-all duration-200
|
||||||
|
${timeFilter === filter.value
|
||||||
|
? 'bg-white text-purple-600 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-gray-400 hover:text-gray-600 cursor-help transition-colors"
|
||||||
|
title="Gráfico mostrando a evolução das suas métricas de leitura ao longo das semanas"
|
||||||
|
>
|
||||||
|
<HelpCircle className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pill Buttons */}
|
||||||
|
<div className="flex flex-wrap gap-2 p-1">
|
||||||
|
{METRICS_CONFIG.map(metric => (
|
||||||
|
<button
|
||||||
|
key={metric.key}
|
||||||
|
onClick={() => toggleMetric(metric.key)}
|
||||||
|
className={`
|
||||||
|
px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ease-in-out
|
||||||
|
${visibleMetrics.has(metric.key)
|
||||||
|
? 'shadow-md transform -translate-y-px'
|
||||||
|
: 'bg-gray-50 text-gray-500 hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-opacity-50
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: visibleMetrics.has(metric.key) ? metric.color : undefined,
|
||||||
|
color: visibleMetrics.has(metric.key) ? 'white' : undefined,
|
||||||
|
boxShadow: visibleMetrics.has(metric.key) ? '0 2px 4px rgba(0,0,0,0.1)' : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${visibleMetrics.has(metric.key) ? 'bg-white' : 'bg-gray-400'}`}></span>
|
||||||
|
{metric.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-[400px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<ComposedChart data={filteredData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.3} />
|
||||||
|
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.1} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke="#f0f0f0"
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="week"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
dy={10}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
yAxisId="left"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
dx={-10}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
yAxisId="right"
|
||||||
|
orientation="right"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||||
|
dx={10}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number, name: string) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
verticalAlign="top"
|
||||||
|
align="right"
|
||||||
|
iconType="circle"
|
||||||
|
wrapperStyle={{
|
||||||
|
paddingBottom: '20px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{METRICS_CONFIG.map(metric => (
|
||||||
|
visibleMetrics.has(metric.key) && (
|
||||||
|
<Line
|
||||||
|
key={metric.key}
|
||||||
|
yAxisId="left"
|
||||||
|
type="monotone"
|
||||||
|
dataKey={metric.key}
|
||||||
|
stroke={metric.color}
|
||||||
|
name={metric.name}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
dot={{ strokeWidth: 2, r: 4, fill: 'white' }}
|
||||||
|
activeDot={{ r: 6, strokeWidth: 2 }}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
<Bar
|
||||||
|
yAxisId="right"
|
||||||
|
dataKey="minutesRead"
|
||||||
|
name="Minutos Lidos"
|
||||||
|
fill="url(#barGradient)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
maxBarSize={50}
|
||||||
|
/>
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import type { Story, Student } from '../../types/database';
|
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 {
|
interface DashboardMetrics {
|
||||||
totalStories: number;
|
totalStories: number;
|
||||||
@ -30,18 +30,6 @@ interface WeeklyMetrics {
|
|||||||
minutesRead: number;
|
minutesRead: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WeeklyData {
|
|
||||||
count: number;
|
|
||||||
fluency: number;
|
|
||||||
pronunciation: number;
|
|
||||||
accuracy: number;
|
|
||||||
comprehension: number;
|
|
||||||
wordsPerMinute: number;
|
|
||||||
pauses: number;
|
|
||||||
errors: number;
|
|
||||||
minutesRead: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Recording {
|
interface Recording {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
fluency_score: number;
|
fluency_score: number;
|
||||||
@ -53,35 +41,18 @@ interface Recording {
|
|||||||
error_count: number;
|
error_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetricConfig {
|
interface WeeklyData {
|
||||||
key: string;
|
count: number;
|
||||||
name: string;
|
fluency: number;
|
||||||
color: string;
|
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() {
|
export function StudentDashboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [student, setStudent] = React.useState<Student | null>(null);
|
const [student, setStudent] = React.useState<Student | null>(null);
|
||||||
@ -101,10 +72,6 @@ export function StudentDashboardPage() {
|
|||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [recentStories, setRecentStories] = React.useState<Story[]>([]);
|
const [recentStories, setRecentStories] = React.useState<Story[]>([]);
|
||||||
const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>(
|
|
||||||
new Set(METRICS_CONFIG.map(metric => metric.key))
|
|
||||||
);
|
|
||||||
const [timeFilter, setTimeFilter] = React.useState<TimeFilter>('12m');
|
|
||||||
|
|
||||||
const processWeeklyMetrics = (recordings: Recording[]) => {
|
const processWeeklyMetrics = (recordings: Recording[]) => {
|
||||||
const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, 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));
|
.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(() => {
|
React.useEffect(() => {
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
try {
|
try {
|
||||||
@ -283,10 +224,10 @@ export function StudentDashboardPage() {
|
|||||||
|
|
||||||
// Calcular médias
|
// Calcular médias
|
||||||
setMetrics({
|
setMetrics({
|
||||||
totalStories: allStoriesData.length, // Usando o total de histórias
|
totalStories: allStoriesData.length,
|
||||||
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
|
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
|
||||||
totalReadingTime: recordings.length * 2, // Exemplo: 2 minutos por gravação
|
totalReadingTime: recordings.length * 2,
|
||||||
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)), // Exemplo: nível baseado na fluência
|
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)),
|
||||||
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
|
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
|
||||||
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
|
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
|
||||||
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
|
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
|
||||||
@ -339,174 +280,6 @@ export function StudentDashboardPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMetricsChart = () => {
|
|
||||||
const filteredData = filterDataByTime(weeklyMetrics);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 mb-8">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Evolução das Métricas por Semana</h2>
|
|
||||||
<p className="text-sm text-gray-500">Acompanhe seu progresso ao longo do tempo</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Filtro de Período */}
|
|
||||||
<div className="flex items-center gap-2 bg-gray-50 p-1 rounded-lg">
|
|
||||||
<Calendar className="h-4 w-4 text-gray-500" />
|
|
||||||
{TIME_FILTERS.map(filter => (
|
|
||||||
<button
|
|
||||||
key={filter.value}
|
|
||||||
onClick={() => setTimeFilter(filter.value)}
|
|
||||||
className={`
|
|
||||||
px-3 py-1 rounded-md text-sm font-medium transition-all duration-200
|
|
||||||
${timeFilter === filter.value
|
|
||||||
? 'bg-white text-purple-600 shadow-sm'
|
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{filter.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-gray-400 hover:text-gray-600 cursor-help transition-colors"
|
|
||||||
title="Gráfico mostrando a evolução das suas métricas de leitura ao longo das semanas"
|
|
||||||
>
|
|
||||||
<HelpCircle className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pill Buttons */}
|
|
||||||
<div className="flex flex-wrap gap-2 p-1">
|
|
||||||
{METRICS_CONFIG.map(metric => (
|
|
||||||
<button
|
|
||||||
key={metric.key}
|
|
||||||
onClick={() => toggleMetric(metric.key)}
|
|
||||||
className={`
|
|
||||||
px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ease-in-out
|
|
||||||
${visibleMetrics.has(metric.key)
|
|
||||||
? 'shadow-md transform -translate-y-px'
|
|
||||||
: 'bg-gray-50 text-gray-500 hover:bg-gray-100'
|
|
||||||
}
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-opacity-50
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: visibleMetrics.has(metric.key) ? metric.color : undefined,
|
|
||||||
color: visibleMetrics.has(metric.key) ? 'white' : undefined,
|
|
||||||
boxShadow: visibleMetrics.has(metric.key) ? '0 2px 4px rgba(0,0,0,0.1)' : undefined
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className={`w-2 h-2 rounded-full ${visibleMetrics.has(metric.key) ? 'bg-white' : 'bg-gray-400'}`}></span>
|
|
||||||
{metric.name}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[400px] w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<ComposedChart data={filteredData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.3} />
|
|
||||||
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.1} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
vertical={false}
|
|
||||||
stroke="#f0f0f0"
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="week"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
|
||||||
dy={10}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="left"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
|
||||||
dx={-10}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="right"
|
|
||||||
orientation="right"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
|
||||||
dx={10}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={(value: number, name: string) => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<Legend
|
|
||||||
verticalAlign="top"
|
|
||||||
align="right"
|
|
||||||
iconType="circle"
|
|
||||||
wrapperStyle={{
|
|
||||||
paddingBottom: '20px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{METRICS_CONFIG.map(metric => (
|
|
||||||
visibleMetrics.has(metric.key) && (
|
|
||||||
<Line
|
|
||||||
key={metric.key}
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey={metric.key}
|
|
||||||
stroke={metric.color}
|
|
||||||
name={metric.name}
|
|
||||||
strokeWidth={2.5}
|
|
||||||
dot={{ strokeWidth: 2, r: 4, fill: 'white' }}
|
|
||||||
activeDot={{ r: 6, strokeWidth: 2 }}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
))}
|
|
||||||
<Bar
|
|
||||||
yAxisId="right"
|
|
||||||
dataKey="minutesRead"
|
|
||||||
name="Minutos Lidos"
|
|
||||||
fill="url(#barGradient)"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
isAnimationActive={false}
|
|
||||||
maxBarSize={50}
|
|
||||||
/>
|
|
||||||
</ComposedChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Cabeçalho */}
|
{/* Cabeçalho */}
|
||||||
@ -736,7 +509,7 @@ export function StudentDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gráfico de Evolução */}
|
{/* Gráfico de Evolução */}
|
||||||
{renderMetricsChart()}
|
<MetricsChart data={weeklyMetrics} className="mb-8" />
|
||||||
|
|
||||||
{/* Histórias Recentes */}
|
{/* Histórias Recentes */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user