feat: adiciona componentes de visualização para métricas de escrita

Cria WritingMetricsSection para exibição de cards de métricas
Cria WritingMetricsChart para visualização da evolução
Integra novos componentes ao StudentDashboardPage
Mantém consistência visual com métricas de leitura
type: feat
scope: metrics
breaking: false
This commit is contained in:
Lucas Santana 2025-02-07 10:55:24 -03:00
parent 8c6e6aedd3
commit 190777dcd0
3 changed files with 402 additions and 4 deletions

View File

@ -0,0 +1,237 @@
import React from 'react';
import { Calendar, HelpCircle } from 'lucide-react';
import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts';
import type { WeeklyWritingMetrics } from '@/types/metrics';
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: 'score', name: 'Nota Geral', color: '#6366f1' },
{ key: 'adequacy', name: 'Adequação', color: '#f43f5e' },
{ key: 'coherence', name: 'Coerência', color: '#0ea5e9' },
{ key: 'cohesion', name: 'Coesão', color: '#10b981' },
{ key: 'vocabulary', name: 'Vocabulário', color: '#8b5cf6' },
{ key: 'grammar', name: 'Gramática', color: '#f59e0b' }
];
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 WritingMetricsChartProps {
data: WeeklyWritingMetrics[];
className?: string;
}
export function WritingMetricsChart({ data, className = '' }: WritingMetricsChartProps) {
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: WeeklyWritingMetrics[]): WeeklyWritingMetrics[] => {
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 da Escrita por Semana</h2>
<p className="text-sm text-gray-500">Acompanhe seu progresso na escrita 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 escrita 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 } = {
score: 'Nota Geral',
adequacy: 'Adequação',
coherence: 'Coerência',
cohesion: 'Coesão',
vocabulary: 'Vocabulário',
grammar: 'Gramática',
minutesWriting: 'Minutos Escrevendo'
};
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="minutesWriting"
name="Minutos Escrevendo"
fill="url(#barGradient)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
maxBarSize={50}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,143 @@
import React from 'react';
import { BookOpen, Clock, TrendingUp, Award, Target, Brain, Gauge, Sparkles, Puzzle, Pencil, HelpCircle } from 'lucide-react';
import { MetricCard } from './MetricCard';
import type { WritingMetrics } from '@/types/metrics';
interface WritingMetricsSectionProps {
data: WritingMetrics;
className?: string;
}
const MAIN_METRICS = [
{
key: 'totalEssays',
title: 'Total de Redações',
getValue: (data: WritingMetrics) => data.totalEssays,
icon: BookOpen,
iconColor: 'text-purple-600',
iconBgColor: 'bg-purple-100'
},
{
key: 'averageScore',
title: 'Nota Média',
getValue: (data: WritingMetrics) => `${data.averageScore}%`,
icon: TrendingUp,
iconColor: 'text-green-600',
iconBgColor: 'bg-green-100'
},
{
key: 'totalEssaysTime',
title: 'Tempo de Escrita',
getValue: (data: WritingMetrics) => `${data.totalEssaysTime}min`,
icon: Clock,
iconColor: 'text-blue-600',
iconBgColor: 'bg-blue-100'
},
{
key: 'currentWritingLevel',
title: 'Nível de Escrita',
getValue: (data: WritingMetrics) => data.currentWritingLevel,
icon: Award,
iconColor: 'text-yellow-600',
iconBgColor: 'bg-yellow-100'
}
];
const DETAILED_METRICS = [
{
key: 'averageAdequacy',
title: 'Adequação ao Tema',
getValue: (data: WritingMetrics) => `${data.averageAdequacy}%`,
icon: Target,
iconColor: 'text-indigo-600',
iconBgColor: 'bg-indigo-100',
tooltip: 'Avalia o quanto sua redação está alinhada com o tema e gênero propostos'
},
{
key: 'averageCoherence',
title: 'Coerência',
getValue: (data: WritingMetrics) => `${data.averageCoherence}%`,
icon: Brain,
iconColor: 'text-pink-600',
iconBgColor: 'bg-pink-100',
tooltip: 'Indica a clareza e lógica no desenvolvimento das ideias do texto'
},
{
key: 'averageCohesion',
title: 'Coesão',
getValue: (data: WritingMetrics) => `${data.averageCohesion}%`,
icon: Puzzle,
iconColor: 'text-orange-600',
iconBgColor: 'bg-orange-100',
tooltip: 'Avalia o uso adequado de conectivos e elementos de ligação entre as partes do texto'
},
{
key: 'averageVocabulary',
title: 'Vocabulário',
getValue: (data: WritingMetrics) => `${data.averageVocabulary}%`,
icon: Sparkles,
iconColor: 'text-cyan-600',
iconBgColor: 'bg-cyan-100',
tooltip: 'Analisa a riqueza e adequação do vocabulário utilizado'
},
{
key: 'averageGrammar',
title: 'Gramática',
getValue: (data: WritingMetrics) => `${data.averageGrammar}%`,
icon: Pencil,
iconColor: 'text-amber-600',
iconBgColor: 'bg-amber-100',
tooltip: 'Avalia o uso correto das regras gramaticais e ortográficas'
}
];
export function WritingMetricsSection({ data, className = '' }: WritingMetricsSectionProps) {
return (
<div className={className}>
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Escrita</h2>
{/* Métricas Principais */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{MAIN_METRICS.map(metric => (
<MetricCard
key={metric.key}
title={metric.title}
value={metric.getValue(data)}
icon={metric.icon}
iconColor={metric.iconColor}
iconBgColor={metric.iconBgColor}
/>
))}
</div>
{/* Métricas Detalhadas */}
<div>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-lg font-semibold text-gray-900">Métricas Detalhadas de Escrita</h3>
<div
className="text-gray-400 hover:text-gray-600 cursor-help transition-colors"
title="Estas métricas são calculadas com base em todas as suas redações, fornecendo uma visão detalhada do seu progresso na escrita"
>
<HelpCircle className="h-4 w-4" />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{DETAILED_METRICS.map(metric => (
<MetricCard
key={metric.key}
title={metric.title}
value={metric.getValue(data)}
icon={metric.icon}
iconColor={metric.iconColor}
iconBgColor={metric.iconBgColor}
tooltip={metric.tooltip}
/>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -5,6 +5,8 @@ import { supabase } from '../../lib/supabase';
import type { Story, Student } from '../../types/database';
import { MetricsChart } from '@/components/dashboard/MetricsChart';
import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics';
import { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection';
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart';
import type {
DashboardMetrics as DashboardMetricsType,
DashboardWeeklyMetrics,
@ -342,11 +344,27 @@ export function StudentDashboardPage() {
</div>
</div>
{/* Métricas */}
<DashboardMetrics data={metrics.reading} />
{/* Seção de Métricas de Leitura */}
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Leitura</h2>
{/* Métricas de Leitura */}
<DashboardMetrics data={metrics.reading} className="mb-8" />
{/* Gráfico de Evolução */}
<MetricsChart data={weeklyMetrics.reading} className="mb-8" />
{/* Gráfico de Evolução da Leitura */}
<MetricsChart data={weeklyMetrics.reading} className="mb-8" />
</div>
{/* Seção de Métricas de Escrita */}
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Escrita</h2>
{/* Métricas de Escrita */}
<WritingMetricsSection data={metrics.writing} className="mb-8" />
{/* Gráfico de Evolução da Escrita */}
<WritingMetricsChart data={weeklyMetrics.writing} className="mb-8" />
</div>
{/* Histórias Recentes */}
<div>