mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 06:17:56 +00:00
feat: adiciona gráfico de evolução das métricas na dashboard
Implementa um novo gráfico combinado (linha + barras) para visualização da evolução das métricas do aluno ao longo do tempo. O gráfico inclui: - Visualização das métricas principais (fluência, pronúncia, etc.) - Gráfico de barras com minutos lidos por semana - Botões interativos para filtrar métricas - Design moderno com gradientes e animações - Agrupamento automático por semana - Layout responsivo Principais mudanças técnicas: - Adiciona Recharts para visualização de dados - Implementa processamento de métricas semanais - Otimiza carregamento e agrupamento de dados
This commit is contained in:
parent
18bc42d280
commit
c029aab50f
19
CHANGELOG.md
19
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
|
- Separação entre consulta de métricas e consulta de exibição
|
||||||
- Otimização no cálculo de médias das métricas
|
- Otimização no cálculo de médias das métricas
|
||||||
- Melhoria na organização do código com comentários explicativos
|
- 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
|
||||||
|
|||||||
283
package-lock.json
generated
283
package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"@supabase/supabase-js": "^2.39.7",
|
"@supabase/supabase-js": "^2.39.7",
|
||||||
"@tanstack/react-query": "^5.62.8",
|
"@tanstack/react-query": "^5.62.8",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
|
"@tremor/react": "^3.18.7",
|
||||||
"@types/ioredis": "^4.28.10",
|
"@types/ioredis": "^4.28.10",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/next": "^8.0.7",
|
"@types/next": "^8.0.7",
|
||||||
@ -36,7 +37,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.1",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"shadcn-ui": "^0.9.4",
|
"shadcn-ui": "^0.9.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
@ -1020,6 +1021,106 @@
|
|||||||
"node": ">=18.x"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz",
|
"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": {
|
"node_modules/@react-email/render": {
|
||||||
"version": "0.0.16",
|
"version": "0.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz",
|
||||||
@ -2640,6 +2806,27 @@
|
|||||||
"react-dom": "^18.2.0"
|
"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": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.21.0",
|
"version": "1.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
|
||||||
@ -3067,6 +3254,33 @@
|
|||||||
"react": "^18 || ^19"
|
"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": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
"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": {
|
"node_modules/@ts-morph/common": {
|
||||||
"version": "0.19.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz",
|
||||||
@ -4914,6 +5147,16 @@
|
|||||||
"node": ">= 12"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
@ -7912,6 +8155,20 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
@ -8103,6 +8360,16 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@ -8165,16 +8432,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/recharts": {
|
"node_modules/recharts": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
|
||||||
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
|
"integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"eventemitter3": "^4.0.1",
|
"eventemitter3": "^4.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-smooth": "^4.0.0",
|
"react-smooth": "^4.0.4",
|
||||||
"recharts-scale": "^0.4.4",
|
"recharts-scale": "^0.4.4",
|
||||||
"tiny-invariant": "^1.3.1",
|
"tiny-invariant": "^1.3.1",
|
||||||
"victory-vendor": "^36.6.8"
|
"victory-vendor": "^36.6.8"
|
||||||
@ -8927,6 +9194,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
"@supabase/supabase-js": "^2.39.7",
|
"@supabase/supabase-js": "^2.39.7",
|
||||||
"@tanstack/react-query": "^5.62.8",
|
"@tanstack/react-query": "^5.62.8",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
|
"@tremor/react": "^3.18.7",
|
||||||
"@types/ioredis": "^4.28.10",
|
"@types/ioredis": "^4.28.10",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/next": "^8.0.7",
|
"@types/next": "^8.0.7",
|
||||||
@ -43,7 +44,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.1",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"shadcn-ui": "^0.9.4",
|
"shadcn-ui": "^0.9.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Plus, BookOpen, Clock, TrendingUp, Award, Mic, Target, Brain, Gauge, Pa
|
|||||||
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';
|
||||||
|
|
||||||
interface DashboardMetrics {
|
interface DashboardMetrics {
|
||||||
totalStories: number;
|
totalStories: number;
|
||||||
@ -17,6 +18,55 @@ interface DashboardMetrics {
|
|||||||
averageErrors: number;
|
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() {
|
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);
|
||||||
@ -32,9 +82,72 @@ export function StudentDashboardPage() {
|
|||||||
averagePauses: 0,
|
averagePauses: 0,
|
||||||
averageErrors: 0
|
averageErrors: 0
|
||||||
});
|
});
|
||||||
|
const [weeklyMetrics, setWeeklyMetrics] = React.useState<WeeklyMetrics[]>([]);
|
||||||
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 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(() => {
|
React.useEffect(() => {
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
@ -64,24 +177,58 @@ export function StudentDashboardPage() {
|
|||||||
|
|
||||||
if (allStoriesError) throw allStoriesError;
|
if (allStoriesError) throw allStoriesError;
|
||||||
|
|
||||||
// Buscar histórias recentes para exibição
|
// Buscar todas as gravações do aluno
|
||||||
const { data: recentStoriesData, error: storiesError } = await supabase
|
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')
|
.from('stories')
|
||||||
.select('*')
|
.select(`
|
||||||
|
*,
|
||||||
|
cover:story_pages!inner(
|
||||||
|
image_url
|
||||||
|
)
|
||||||
|
`)
|
||||||
.eq('student_id', session.user.id)
|
.eq('student_id', session.user.id)
|
||||||
|
.eq('story_pages.page_number', 1)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(6);
|
.limit(6);
|
||||||
|
|
||||||
if (storiesError) throw storiesError;
|
if (storiesError) throw storiesError;
|
||||||
|
|
||||||
// Buscar todas as gravações completadas do aluno
|
// Transformar os dados para definir a propriedade 'cover'
|
||||||
const { data: recordings, error: recordingsError } = await supabase
|
const mappedData = (stories || []).map(story => {
|
||||||
.from('story_recordings')
|
let coverObj = null;
|
||||||
.select('*')
|
if (story.cover) {
|
||||||
.eq('status', 'completed')
|
coverObj = Array.isArray(story.cover) ? story.cover[0] : story.cover;
|
||||||
.in('story_id', allStoriesData.map(story => story.id));
|
} 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
|
// Calcular métricas baseadas nas gravações
|
||||||
if (recordings && recordings.length > 0) {
|
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) {
|
} catch (err) {
|
||||||
console.error('Erro ao carregar dashboard:', err);
|
console.error('Erro ao carregar dashboard:', err);
|
||||||
setError('Não foi possível carregar seus dados');
|
setError('Não foi possível carregar seus dados');
|
||||||
@ -198,6 +309,153 @@ export function StudentDashboardPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderMetricsChart = () => {
|
||||||
|
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-2">
|
||||||
|
<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={weeklyMetrics} 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 */}
|
||||||
@ -426,6 +684,9 @@ export function StudentDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Gráfico de Evolução */}
|
||||||
|
{renderMetricsChart()}
|
||||||
|
|
||||||
{/* Histórias Recentes */}
|
{/* Histórias Recentes */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user