diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c8442..50631f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -216,3 +216,22 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). - Separação entre consulta de métricas e consulta de exibição - Otimização no cálculo de médias das métricas - Melhoria na organização do código com comentários explicativos + +## [Não publicado] + +### Adicionado +- Novo gráfico de evolução das métricas na dashboard do aluno + - Visualização combinada de linhas e barras + - Métricas de fluência, pronúncia, precisão, compreensão e palavras por minuto + - Gráfico de barras mostrando minutos lidos por semana + - Botões interativos para filtrar métricas + - Design moderno com gradientes e animações suaves + - Tooltip personalizado com informações detalhadas + - Agrupamento automático por semana + - Layout responsivo e adaptável + +### Técnico +- Implementação do Recharts para visualização de dados +- Novo sistema de processamento de métricas semanais +- Otimização do carregamento de dados com agrupamento eficiente +- Integração com o tema existente do sistema diff --git a/package-lock.json b/package-lock.json index ce517dc..cb4047c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@supabase/supabase-js": "^2.39.7", "@tanstack/react-query": "^5.62.8", "@testing-library/react": "^16.1.0", + "@tremor/react": "^3.18.7", "@types/ioredis": "^4.28.10", "@types/jest": "^29.5.14", "@types/next": "^8.0.7", @@ -36,7 +37,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-router-dom": "^6.28.0", - "recharts": "^2.15.0", + "recharts": "^2.15.1", "resend": "^3.2.0", "shadcn-ui": "^0.9.4", "tailwind-merge": "^2.6.0", @@ -1020,6 +1021,106 @@ "node": ">=18.x" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.19.2.tgz", + "integrity": "sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^1.3.0", + "aria-hidden": "^1.1.3", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz", + "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@headlessui/react/node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@headlessui/react/node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -2622,6 +2723,71 @@ } } }, + "node_modules/@react-aria/focus": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz", + "integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz", + "integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-email/render": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", @@ -2640,6 +2806,27 @@ "react-dom": "^18.2.0" } }, + "node_modules/@react-stately/utils": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", + "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz", + "integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@remix-run/router": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", @@ -3067,6 +3254,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.12.0.tgz", + "integrity": "sha512-6krceiPN07kpxXmU6m8AY7EL0X1gHLu8m3nJdh4phvktzVNxkQfBmSwnRUpoUjGQO1PAn8wSAhYaL8hY1cS1vw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.12.0.tgz", + "integrity": "sha512-7mDINtua3v/pOnn6WUmuT9dPXYSO7WidFej7JzoAfqEOcbbpt/iZ1WPqd+eg+FnrL9nUJK8radqj4iAU51Zchg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -3291,6 +3505,25 @@ } } }, + "node_modules/@tremor/react": { + "version": "3.18.7", + "resolved": "https://registry.npmjs.org/@tremor/react/-/react-3.18.7.tgz", + "integrity": "sha512-nmqvf/1m0GB4LXc7v2ftdfSLoZhy5WLrhV6HNf0SOriE6/l8WkYeWuhQq8QsBjRi94mUIKLJ/VC3/Y/pj6VubQ==", + "license": "Apache 2.0", + "dependencies": { + "@floating-ui/react": "^0.19.2", + "@headlessui/react": "2.2.0", + "date-fns": "^3.6.0", + "react-day-picker": "^8.10.1", + "react-transition-state": "^2.1.2", + "recharts": "^2.13.3", + "tailwind-merge": "^2.5.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/@ts-morph/common": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", @@ -4914,6 +5147,16 @@ "node": ">= 12" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -7912,6 +8155,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -8103,6 +8360,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-transition-state": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.3.0.tgz", + "integrity": "sha512-OucQRyIpeq5g1/5qSBJH4p4U+SzYEoB3MsAXXz3ty116rPqG4jhwrzInImZ2gFi4XOffeLu6HNCZvkCM3DKaeg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8165,16 +8432,16 @@ } }, "node_modules/recharts": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", - "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", - "react-smooth": "^4.0.0", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" @@ -8927,6 +9194,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", diff --git a/package.json b/package.json index efa942b..e88baff 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@supabase/supabase-js": "^2.39.7", "@tanstack/react-query": "^5.62.8", "@testing-library/react": "^16.1.0", + "@tremor/react": "^3.18.7", "@types/ioredis": "^4.28.10", "@types/jest": "^29.5.14", "@types/next": "^8.0.7", @@ -43,7 +44,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-router-dom": "^6.28.0", - "recharts": "^2.15.0", + "recharts": "^2.15.1", "resend": "^3.2.0", "shadcn-ui": "^0.9.4", "tailwind-merge": "^2.6.0", diff --git a/src/pages/student-dashboard/StudentDashboardPage.tsx b/src/pages/student-dashboard/StudentDashboardPage.tsx index 5a6618f..4494c54 100644 --- a/src/pages/student-dashboard/StudentDashboardPage.tsx +++ b/src/pages/student-dashboard/StudentDashboardPage.tsx @@ -3,6 +3,7 @@ import { Plus, BookOpen, Clock, TrendingUp, Award, Mic, Target, Brain, Gauge, Pa 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'; interface DashboardMetrics { totalStories: number; @@ -17,6 +18,55 @@ interface DashboardMetrics { averageErrors: number; } +interface WeeklyMetrics { + week: string; + fluency: number; + pronunciation: number; + accuracy: number; + comprehension: number; + wordsPerMinute: number; + pauses: number; + errors: 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 { + created_at: string; + fluency_score: number; + pronunciation_score: number; + accuracy_score: number; + comprehension_score: number; + words_per_minute: number; + pause_count: number; + error_count: number; +} + +interface MetricConfig { + key: string; + name: string; + color: string; +} + +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' } +]; + export function StudentDashboardPage() { const navigate = useNavigate(); const [student, setStudent] = React.useState(null); @@ -32,9 +82,72 @@ export function StudentDashboardPage() { averagePauses: 0, averageErrors: 0 }); + const [weeklyMetrics, setWeeklyMetrics] = React.useState([]); 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 processWeeklyMetrics = (recordings: Recording[]) => { + const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => { + const date = new Date(recording.created_at); + const week = `${date.getFullYear()}-W${Math.ceil((date.getDate() + date.getDay()) / 7)}`; + + if (!acc[week]) { + acc[week] = { + count: 0, + fluency: 0, + pronunciation: 0, + accuracy: 0, + comprehension: 0, + wordsPerMinute: 0, + pauses: 0, + errors: 0, + minutesRead: 0 + }; + } + + acc[week].count += 1; + acc[week].fluency += recording.fluency_score; + acc[week].pronunciation += recording.pronunciation_score; + acc[week].accuracy += recording.accuracy_score; + acc[week].comprehension += recording.comprehension_score; + acc[week].wordsPerMinute += recording.words_per_minute; + acc[week].pauses += recording.pause_count; + acc[week].errors += recording.error_count; + acc[week].minutesRead += 2; + + return acc; + }, {}); + + return Object.entries(weeklyData) + .map(([week, data]: [string, WeeklyData]) => ({ + week, + fluency: Math.round(data.fluency / data.count), + pronunciation: Math.round(data.pronunciation / data.count), + accuracy: Math.round(data.accuracy / data.count), + comprehension: Math.round(data.comprehension / data.count), + wordsPerMinute: Math.round(data.wordsPerMinute / data.count), + pauses: Math.round(data.pauses / data.count), + errors: Math.round(data.errors / data.count), + minutesRead: data.minutesRead + })) + .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; + }); + }; React.useEffect(() => { const fetchDashboardData = async () => { @@ -64,24 +177,58 @@ export function StudentDashboardPage() { if (allStoriesError) throw allStoriesError; - // Buscar histórias recentes para exibição - const { data: recentStoriesData, error: storiesError } = await supabase + // Buscar todas as gravações do aluno + const { data: recordings, error: recordingsError } = await supabase + .from('story_recordings') + .select(` + *, + story:stories(id, student_id) + `) + .eq('stories.student_id', session.user.id) + .eq('status', 'completed') + .order('created_at', { ascending: true }); + + if (recordingsError) throw recordingsError; + + // Processar métricas semanais + const weeklyData = processWeeklyMetrics(recordings); + setWeeklyMetrics(weeklyData); + + // Buscar histórias recentes com a capa definida + const { data: stories, error: storiesError } = await supabase .from('stories') - .select('*') + .select(` + *, + cover:story_pages!inner( + image_url + ) + `) .eq('student_id', session.user.id) + .eq('story_pages.page_number', 1) .order('created_at', { ascending: false }) .limit(6); if (storiesError) throw storiesError; - // Buscar todas as gravações completadas do aluno - const { data: recordings, error: recordingsError } = await supabase - .from('story_recordings') - .select('*') - .eq('status', 'completed') - .in('story_id', allStoriesData.map(story => story.id)); + // Transformar os dados para definir a propriedade 'cover' + const mappedData = (stories || []).map(story => { + let coverObj = null; + if (story.cover) { + coverObj = Array.isArray(story.cover) ? story.cover[0] : story.cover; + } else if (story.pages && story.pages.length > 0) { + coverObj = story.pages[0]; + } - if (recordingsError) throw recordingsError; + if (coverObj && typeof coverObj === 'object' && coverObj.image_url) { + return { ...story, cover: coverObj }; + } else if (coverObj && typeof coverObj === 'string') { + return { ...story, cover: { image_url: coverObj } }; + } + + return { ...story, cover: null }; + }); + + setRecentStories(mappedData); // Calcular métricas baseadas nas gravações if (recordings && recordings.length > 0) { @@ -119,42 +266,6 @@ export function StudentDashboardPage() { }); } - // Buscar histórias recentes com a capa definida (mantendo o limite de 6 para exibição) - const { data, error } = await supabase - .from('stories') - .select(` - *, - cover:story_pages!inner( - image_url - ) - `) - .eq('student_id', session.user.id) - .eq('story_pages.page_number', 1) // Garante que pegamos a primeira página - .order('created_at', { ascending: false }) - .limit(6); - - if (error) throw error; - - // Transformar os dados para definir a propriedade 'cover' de forma robusta - const mappedData = (data || []).map(story => { - let coverObj = null; - if (story.cover) { - coverObj = Array.isArray(story.cover) ? story.cover[0] : story.cover; - } else if (story.pages && story.pages.length > 0) { - coverObj = story.pages[0]; - } - - if (coverObj && typeof coverObj === 'object' && coverObj.image_url) { - return { ...story, cover: coverObj }; - } else if (coverObj && typeof coverObj === 'string') { - return { ...story, cover: { image_url: coverObj } }; - } - - return { ...story, cover: null }; - }); - - setRecentStories(mappedData); - } catch (err) { console.error('Erro ao carregar dashboard:', err); setError('Não foi possível carregar seus dados'); @@ -198,6 +309,153 @@ export function StudentDashboardPage() { ); } + const renderMetricsChart = () => { + return ( +
+
+
+
+

Evolução das Métricas por Semana

+

Acompanhe seu progresso ao longo do tempo

+
+
+
+ +
+
+
+ + {/* 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 */} @@ -426,6 +684,9 @@ export function StudentDashboardPage() {
+ {/* Gráfico de Evolução */} + {renderMetricsChart()} + {/* Histórias Recentes */}