Compare commits

..

5 Commits

Author SHA1 Message Date
Lucas Santana
e1a99f32f5 fix: Consolidando StudentDashbord
Some checks are pending
Docker Build and Push / build (push) Waiting to run
2025-01-17 17:25:30 -03:00
Lucas Santana
18cf6a2495 fix: Adicionando tracking na página de Demo 2025-01-17 16:07:07 -03:00
Lucas Santana
6a1a471ce5 fix: Corrigindo deduplicação de eventos no Rudderstack 2025-01-17 12:51:36 -03:00
Lucas Santana
bcbdd07a41 fix: PageTracker geral 2025-01-17 12:39:10 -03:00
Lucas Santana
98411b2aa1 feat: Documentação do Analytics 2025-01-17 11:23:20 -03:00
15 changed files with 685 additions and 473 deletions

View File

@ -12,6 +12,8 @@ import { AuthProvider } from './contexts/AuthContext'
import { useNavigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Toaster } from './components/ui/toaster';
import { router } from './routes';
import { RouterProvider } from 'react-router-dom';
type AppStep =
| 'welcome'
@ -88,6 +90,7 @@ export function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<RouterProvider router={router} />
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
{step === 'welcome' && (
<WelcomePage

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useRudderstack } from '../../hooks/useRudderstack';
import { useLocation } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
@ -7,82 +7,106 @@ export function PageTracker() {
const location = useLocation();
const { page } = useRudderstack();
const { user } = useAuth();
const lastPageTracked = useRef<string | null>(null);
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
// Coleta informações do dispositivo/navegador
const deviceInfo = {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
deviceType: getDeviceType(),
deviceOrientation: window.screen.orientation.type,
userAgent: navigator.userAgent,
language: navigator.language,
};
// Se já rastreamos esta página, não rastrear novamente
if (lastPageTracked.current === location.pathname) {
return;
}
// Coleta informações de performance
const performanceInfo = {
loadTime: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
domInteractive: window.performance.timing.domInteractive - window.performance.timing.navigationStart,
firstContentfulPaint: getFirstContentfulPaint(),
};
// Limpa o timeout anterior se existir
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Informações da sessão
const sessionInfo = {
sessionStartTime: sessionStorage.getItem('sessionStartTime') || new Date().toISOString(),
isFirstVisit: !localStorage.getItem('returningVisitor'),
lastVisitedPage: sessionStorage.getItem('lastVisitedPage'),
};
// Debounce de 300ms para evitar múltiplos eventos
timeoutRef.current = setTimeout(() => {
// Coleta informações do dispositivo/navegador
const deviceInfo = {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
deviceType: getDeviceType(),
deviceOrientation: window.screen.orientation.type,
userAgent: navigator.userAgent,
language: navigator.language,
};
// Traits do usuário (se autenticado)
const userTraits = user ? {
user_id: user.id,
email: user.email,
school_id: user.user_metadata?.school_id,
class_id: user.user_metadata?.class_id,
name: user.user_metadata?.name,
role: user.user_metadata?.role,
last_updated: user.updated_at,
created_at: user.created_at
} : {};
// Coleta informações de performance
const performanceInfo = {
loadTime: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
domInteractive: window.performance.timing.domInteractive - window.performance.timing.navigationStart,
firstContentfulPaint: getFirstContentfulPaint(),
};
// Envia dados adicionais usando o page() do Rudderstack
page(undefined, {
// Informações da página
path: location.pathname,
search: location.search,
hash: location.hash,
title: document.title,
referrer: document.referrer,
url: window.location.href,
// Informações do dispositivo e navegador
...deviceInfo,
// Informações de performance
...performanceInfo,
// Informações da sessão
...sessionInfo,
// Traits do usuário
...userTraits,
// Metadados adicionais
timestamp: new Date().toISOString(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
const sessionInfo = {
sessionStartTime: sessionStorage.getItem('sessionStartTime') || new Date().toISOString(),
isFirstVisit: !localStorage.getItem('returningVisitor'),
lastVisitedPage: sessionStorage.getItem('lastVisitedPage'),
};
// Atualiza informações da sessão
sessionStorage.setItem('lastVisitedPage', location.pathname);
if (!localStorage.getItem('returningVisitor')) {
localStorage.setItem('returningVisitor', 'true');
}
if (!sessionStorage.getItem('sessionStartTime')) {
sessionStorage.setItem('sessionStartTime', new Date().toISOString());
}
}, [location, page, user]);
// Traits do usuário (se autenticado)
const userTraits = user ? {
user_id: user.id,
email: user.email,
school_id: user.user_metadata?.school_id,
class_id: user.user_metadata?.class_id,
name: user.user_metadata?.name,
role: user.user_metadata?.role,
last_updated: user.updated_at,
created_at: user.created_at
} : {};
// Envia dados adicionais usando o page() do Rudderstack
page(undefined, {
// Informações da página
path: location.pathname,
search: location.search,
hash: location.hash,
title: document.title,
referrer: document.referrer,
url: window.location.href,
// Informações do dispositivo e navegador
...deviceInfo,
// Informações de performance
...performanceInfo,
// Informações da sessão
...sessionInfo,
// Traits do usuário
...userTraits,
// Metadados adicionais
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
// Atualiza a última página rastreada
lastPageTracked.current = location.pathname;
// Atualiza informações da sessão
sessionStorage.setItem('lastVisitedPage', location.pathname);
if (!localStorage.getItem('returningVisitor')) {
localStorage.setItem('returningVisitor', 'true');
}
if (!sessionStorage.getItem('sessionStartTime')) {
sessionStorage.setItem('sessionStartTime', new Date().toISOString());
}
}, 300);
// Cleanup
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [location.pathname, user]); // Reduzido dependências para apenas pathname e user
return null;
}

View File

@ -0,0 +1,330 @@
# 📊 Sistema de Analytics
Este diretório contém a implementação do sistema de analytics do Leiturama, utilizando Rudderstack como principal ferramenta de tracking.
## 🚀 Inicialização
Para inicializar o sistema de analytics corretamente, certifique-se de:
1. Configurar as variáveis de ambiente:
```env
VITE_RUDDERSTACK_WRITE_KEY=seu_write_key
VITE_RUDDERSTACK_DATA_PLANE_URL=sua_url
```
2. Inicializar o analytics antes de usar:
```typescript
// main.tsx
import { analytics } from './lib/analytics';
// Inicializa o analytics antes de renderizar o app
await analytics.init();
// Renderiza o app
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
```
## 🔧 Troubleshooting
### Erros Comuns
1. **Falha na Inicialização**
```typescript
Failed to initialize analytics: Event {...}
```
Possíveis causas:
- Variáveis de ambiente não configuradas
- Script do Rudderstack bloqueado
- Erro na carga do script
Soluções:
- Verifique as variáveis de ambiente
- Verifique se o domínio do Rudderstack está liberado
- Adicione tratamento de erro na inicialização:
```typescript
try {
await analytics.init();
} catch (error) {
console.error('Falha ao inicializar analytics:', error);
// Continue renderizando o app mesmo com falha no analytics
}
```
2. **Eventos Não Rastreados**
Se os eventos não estão sendo rastreados, verifique:
- Se o analytics foi inicializado corretamente
- Se há erros no console
- Se o writeKey e dataPlaneUrl estão corretos
- Se há bloqueadores de rastreamento no navegador
3. **Erros de Tipo**
Se encontrar erros de tipo ao usar os hooks:
- Verifique se está usando as interfaces corretas
- Importe os tipos necessários
- Use as constantes de EVENT_CATEGORIES
## 📦 Componentes
### PageTracker
Componente responsável pelo tracking automático de visualizações de página.
```tsx
// App.tsx
<PageTracker />
```
### GoogleTagManager
Componente para integração com Google Tag Manager.
```tsx
// App.tsx
<GoogleTagManager gtmId="GTM-XXXXXX" />
```
## 🎯 Hooks Disponíveis
### useButtonTracking
Hook para rastreamento de interações com botões e elementos clicáveis.
```typescript
const { trackButtonClick } = useButtonTracking({
category?: string; // Categoria do evento (default: 'interaction')
location?: string; // Localização do botão (default: pathname atual)
});
// Uso:
trackButtonClick('button-id', {
label: 'Botão de Login',
variant: 'primary',
position: 'header',
section: 'auth'
});
```
### useFormTracking
Hook para rastreamento de interações com formulários.
```typescript
const {
trackFormStarted,
trackFormStepCompleted,
trackFormSubmitted,
trackFormError,
trackFormAbandoned,
trackFieldInteraction
} = useFormTracking({
formId: string; // ID único do formulário
formName: string; // Nome descritivo do formulário
category?: string; // Categoria (default: 'form')
});
// Exemplos de Uso:
// Início do formulário
trackFormStarted();
// Completou um passo
trackFormStepCompleted('dados-pessoais', true);
// Submeteu o formulário
trackFormSubmitted(true, {
user_type: 'student'
});
// Erro no formulário
trackFormError('validation', 'Email inválido', 'email');
// Abandonou o formulário
trackFormAbandoned('payment');
// Interação com campo
trackFieldInteraction('email', 'focus');
```
### useStudentTracking
Hook especializado para rastreamento de atividades do estudante.
```typescript
const {
trackStoryGenerated,
trackAudioRecorded,
trackExerciseCompleted,
trackInterestAdded,
trackInterestRemoved
} = useStudentTracking();
// Exemplo: Rastrear geração de história
trackStoryGenerated({
story_id: 'story-123',
theme: 'aventura',
prompt: 'Uma história sobre...',
generation_time: 2.5,
word_count: 300,
student_id: 'student-123'
});
// Exemplo: Rastrear exercício completado
trackExerciseCompleted({
exercise_id: 'ex-123',
story_id: 'story-123',
student_id: 'student-123',
exercise_type: 'pronunciation',
score: 85,
time_spent: 120
});
```
### useErrorTracking
Hook para rastreamento de erros e exceções.
```typescript
const {
trackError,
trackErrorBoundary,
trackApiError
} = useErrorTracking({
category?: string; // Categoria (default: 'error')
userId?: string; // ID do usuário
userEmail?: string; // Email do usuário
});
// Exemplo: Rastrear erro genérico
trackError(error, {
componentName: 'LoginForm',
action: 'submit',
metadata: { attempt: 2 }
});
// Exemplo: Rastrear erro de API
trackApiError(error, '/api/login', 'POST', {
email: 'user@example.com'
});
```
## 📝 Padrões de Nomenclatura
### Eventos
- Use snake_case para nomes de eventos
- Formato: `{objeto}_{ação}`
- Exemplos:
- `button_clicked`
- `form_submitted`
- `story_generated`
- `exercise_completed`
### Propriedades
- Use snake_case para nomes de propriedades
- Categorize propriedades por namespace:
```typescript
{
// Propriedades de página
page_url: string;
page_title: string;
// Propriedades de usuário
user_id: string;
user_type: string;
// Propriedades de elemento
element_type: string;
element_id: string;
// Propriedades de formulário
form_id: string;
form_name: string;
}
```
### Categorias
Categorias predefinidas disponíveis em `EVENT_CATEGORIES`:
- `page`: Eventos de visualização de página
- `user`: Eventos relacionados ao usuário
- `story`: Eventos de histórias
- `exercise`: Eventos de exercícios
- `interaction`: Eventos de interação do usuário
- `error`: Eventos de erro
- `subscription`: Eventos de assinatura
- `auth`: Eventos de autenticação
- `navigation`: Eventos de navegação
- `form`: Eventos de formulário
## 🔨 Exemplos de Implementação
### Botão com Tracking
```typescript
<Button
trackingId="signup-button"
variant="primary"
size="lg"
trackingProperties={{
category: EVENT_CATEGORIES.AUTH,
action: 'signup_click',
label: 'homepage_hero',
position: 'hero_section'
}}
onClick={handleSignup}
>
Cadastre-se Agora
</Button>
```
### Formulário com Tracking
```typescript
<Form
formId="signup-form"
formName="student-signup"
trackingProperties={{
category: EVENT_CATEGORIES.AUTH,
user_type: 'student',
source: 'homepage'
}}
onSubmit={handleSubmit}
>
{/* campos do formulário */}
</Form>
```
### Link com Tracking
```typescript
<Link
to="/dashboard"
trackingId="dashboard-link"
trackingProperties={{
category: EVENT_CATEGORIES.NAVIGATION,
section: 'sidebar',
position: 'top'
}}
>
Dashboard
</Link>
```
### Tracking de Erro em Try/Catch
```typescript
try {
await submitForm(data);
} catch (error) {
errorTracking.trackError(error, {
componentName: 'SignupForm',
action: 'submit',
metadata: {
formData: data,
attempt: retryCount
}
});
}
```

View File

@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import { PageTracker } from '../analytics/PageTracker';
export function BaseLayout() {
return (
<>
<PageTracker />
<Outlet />
</>
);
}

View File

@ -134,7 +134,7 @@ export const EVENT_PROPERTIES = {
STARTED_AT: 'started_at',
COMPLETED_AT: 'completed_at',
DURATION: 'duration',
TIMESTAMP: 'timestamp',
EVENT_TIMESTAMP: 'event_timestamp',
},
// Propriedades de Formulário

View File

@ -36,7 +36,6 @@ export function useButtonTracking(options: ButtonTrackingOptions = {}) {
button_id: buttonId,
category,
location,
timestamp: new Date().toISOString(),
...properties,
// Informações da página
page_title: document.title,

View File

@ -44,7 +44,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
// Informações do ambiente
url: window.location.href,
user_agent: navigator.userAgent,
timestamp: new Date().toISOString(),
});
};
@ -71,7 +70,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
component_stack: componentStack,
url: window.location.href,
user_agent: navigator.userAgent,
timestamp: new Date().toISOString(),
});
};
@ -101,7 +99,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
status_code: error.status || error.statusCode,
request_data: requestData,
url: window.location.href,
timestamp: new Date().toISOString(),
});
};

View File

@ -50,35 +50,30 @@ export function useStudentTracking() {
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
analytics.track('story_generated', {
...properties,
timestamp: new Date().toISOString()
});
};
const trackAudioRecorded = (properties: AudioRecordedProps) => {
analytics.track('audio_recorded', {
...properties,
timestamp: new Date().toISOString()
});
};
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
analytics.track('exercise_completed', {
...properties,
timestamp: new Date().toISOString()
});
};
const trackInterestAdded = (properties: InterestActionProps) => {
analytics.track('interest_added', {
...properties,
timestamp: new Date().toISOString()
});
};
const trackInterestRemoved = (properties: InterestActionProps) => {
analytics.track('interest_removed', {
...properties,
timestamp: new Date().toISOString()
});
};

View File

@ -47,58 +47,68 @@ export class Analytics {
}
// Inicializa o Rudderstack
init(options?: { writeKey?: string; dataPlaneUrl?: string }): Promise<void> {
async init(): Promise<void> {
if (this.initPromise) return this.initPromise;
this.initPromise = new Promise((resolve, reject) => {
const writeKey = options?.writeKey || this.writeKey || import.meta.env.VITE_RUDDERSTACK_WRITE_KEY;
const dataPlaneUrl = options?.dataPlaneUrl || this.dataPlaneUrl || import.meta.env.VITE_RUDDERSTACK_DATA_PLANE_URL;
if (!writeKey || !dataPlaneUrl) {
const error = new Error('Missing Rudderstack configuration');
this.handleError('init', error);
reject(error);
return;
}
try {
const script = document.createElement('script');
script.src = 'https://cdn.rudderlabs.com/v1.1/rudder-analytics.min.js';
script.async = true;
script.onload = () => {
if (window.rudderanalytics) {
window.rudderanalytics.load(writeKey, dataPlaneUrl, {
configUrl: 'https://api.rudderlabs.com',
logLevel: this.debug ? 'DEBUG' : 'ERROR',
this.initPromise = new Promise<void>((resolve, reject) => {
const initializeAnalytics = async () => {
try {
const writeKey = this.writeKey;
const dataPlaneUrl = this.dataPlaneUrl;
if (this.debug) {
console.log('Inicializando analytics com:', {
writeKey,
dataPlaneUrl,
debug: this.debug
});
// Espera um pequeno intervalo para garantir que o Rudderstack está pronto
setTimeout(() => {
this.initialized = true;
this.processEventQueue();
if (this.debug) {
console.log('Rudderstack initialized successfully');
}
resolve();
}, 100);
} else {
const error = new Error('Failed to load Rudderstack');
this.handleError('init', error);
reject(error);
}
};
if (!writeKey || !dataPlaneUrl) {
throw new Error('Analytics configuration missing');
}
script.onerror = (error) => {
await new Promise<void>((scriptResolve, scriptReject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://cdn.rudderlabs.com/v1.1/rudder-analytics.min.js';
script.crossOrigin = 'anonymous';
script.async = true;
script.onload = () => {
if (!window.rudderanalytics) {
scriptReject(new Error('Rudderstack failed to initialize'));
return;
}
window.rudderanalytics.load(writeKey, dataPlaneUrl, {
configUrl: 'https://api.rudderlabs.com',
logLevel: this.debug ? 'DEBUG' : 'ERROR',
secureCookie: true,
crossDomainLinker: true
});
scriptResolve();
};
script.onerror = (error) => {
scriptReject(new Error('Failed to load Rudderstack script: ' + error));
};
document.head.appendChild(script);
});
await new Promise((waitResolve) => setTimeout(waitResolve, 1000));
this.initialized = true;
this.processEventQueue();
resolve();
} catch (error) {
console.error('Analytics initialization failed:', error);
this.handleError('init', error);
reject(error);
};
}
};
document.head.appendChild(script);
} catch (error) {
this.handleError('init', error);
reject(error);
}
initializeAnalytics();
});
return this.initPromise;
@ -210,7 +220,7 @@ export class Analytics {
// Enriquece as propriedades com dados padrão
private enrichEventProperties(properties?: Record<string, unknown>): BaseEventProperties {
const baseProperties: BaseEventProperties = {
[EVENT_PROPERTIES.TIMING.TIMESTAMP]: new Date().toISOString(),
[EVENT_PROPERTIES.TIMING.EVENT_TIMESTAMP]: new Date().toISOString(),
[EVENT_PROPERTIES.PAGE.URL]: window.location.href,
[EVENT_PROPERTIES.PAGE.TITLE]: document.title,
[EVENT_PROPERTIES.PAGE.PATH]: window.location.pathname,
@ -248,5 +258,7 @@ export class Analytics {
// Instância global
export const analytics = new Analytics({
debug: import.meta.env.DEV
debug: import.meta.env.DEV,
writeKey: import.meta.env.VITE_RUDDERSTACK_WRITE_KEY,
dataPlaneUrl: import.meta.env.VITE_RUDDERSTACK_DATA_PLANE_URL
});

View File

@ -1,96 +0,0 @@
import React, { useState } from 'react';
import { AudioRecorderDemo } from '../../components/demo/AudioRecorderDemo';
import { ArrowRight, Sparkles } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export function DemoPage() {
const navigate = useNavigate();
const [demoResult, setDemoResult] = useState<{
fluency?: number;
accuracy?: number;
confidence?: number;
feedback?: string;
} | null>(null);
const handleDemoComplete = (result: typeof demoResult) => {
setDemoResult(result);
};
return (
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Experimente Agora!
</h1>
<p className="text-xl text-gray-600">
Grave um trecho de leitura e veja como nossa IA avalia seu desempenho
</p>
</div>
<div className="bg-white rounded-2xl shadow-xl p-8 mb-8">
<div className="prose max-w-none mb-8">
<h2>Texto Sugerido para Leitura:</h2>
<blockquote className="text-lg text-gray-700 border-l-4 border-purple-300 pl-4">
"O pequeno príncipe sentou-se numa pedra e levantou os olhos para o céu:
Pergunto-me se as estrelas são iluminadas para que cada um possa um dia encontrar a sua."
</blockquote>
</div>
<AudioRecorderDemo onAnalysisComplete={handleDemoComplete} />
</div>
{demoResult && (
<div className="bg-white rounded-2xl shadow-xl p-8 mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2">
<Sparkles className="text-purple-600" />
Resultado da Análise
</h2>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<div className="bg-purple-50 rounded-xl p-6">
<div className="text-3xl font-bold text-purple-600 mb-2">
{demoResult.fluency}%
</div>
<div className="text-gray-600">Fluência</div>
</div>
<div className="bg-purple-50 rounded-xl p-6">
<div className="text-3xl font-bold text-purple-600 mb-2">
{demoResult.accuracy}%
</div>
<div className="text-gray-600">Precisão</div>
</div>
<div className="bg-purple-50 rounded-xl p-6">
<div className="text-3xl font-bold text-purple-600 mb-2">
{demoResult.confidence}%
</div>
<div className="text-gray-600">Confiança</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-6 mb-8">
<h3 className="text-lg font-semibold text-green-800 mb-2">
Feedback da IA
</h3>
<p className="text-green-700">
{demoResult.feedback}
</p>
</div>
<div className="text-center">
<button
onClick={() => navigate('/register/school')}
className="inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 transition"
>
Começar a Usar na Minha Escola
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -3,6 +3,8 @@ import { ArrowLeft, ArrowRight, Mic, Volume2, Share2, ChevronDown, ChevronUp, Lo
import { AudioRecorder } from '../../components/story/AudioRecorder';
import { StoryMetrics } from '../../components/story/StoryMetrics';
import type { StoryRecording } from '../../types/database';
import { analytics } from '../../lib/analytics';
// Separar dados mock em arquivo próprio
const DEMO_DATA = {
@ -126,6 +128,20 @@ export function StoryPageDemo(): JSX.Element {
setTimeout(() => {
setIsRecording(false);
setShowMetrics(true);
// Rastreia quando o demo é completado
analytics.track('demo_completed', {
story_id: DEMO_DATA.story.id,
story_title: DEMO_DATA.story.title,
metrics: {
fluency: DEMO_DATA.recording.fluency_score,
pronunciation: DEMO_DATA.recording.pronunciation_score,
accuracy: DEMO_DATA.recording.accuracy_score,
comprehension: DEMO_DATA.recording.comprehension_score,
words_per_minute: DEMO_DATA.recording.words_per_minute
},
device_type: window.innerWidth < 768 ? 'mobile' : window.innerWidth < 1024 ? 'tablet' : 'desktop'
});
}, 3000); // Simula 3 segundos de "processamento"
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
import {
ArrowRight,
Brain,
BookOpen,
@ -41,28 +41,28 @@ export function EducationalForParents() {
<div className="absolute right-4 top-4 text-gray-600 flex items-center gap-2">
<Clock className="w-4 h-4" />
<span className="text-sm">Tempo de leitura: 5 minutos</span>
</div>
</div>
<h1 className="mt-10 text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Transforme o<br />
Aprendizado em<br />
Uma <span className="text-purple-600">Aventura Mágica</span>
</h1>
</h1>
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-xl">
Histórias educativas personalizadas que encantam e ensinam,
criadas especialmente para o desenvolvimento único do seu filho.
</p>
<div className="mt-10">
<button
<button
onClick={() => window.location.href = '/cadastro'}
className="rounded-xl bg-purple-600 px-8 py-4 text-base font-semibold text-white shadow-sm hover:bg-purple-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-purple-600 flex items-center gap-2"
>
Comece Sua Aventura Mágica Grátis
>
Comece Sua Aventura Mágica Grátis
<ArrowRight className="w-5 h-5" />
</button>
</div>
</button>
</div>
<div className="mt-10 flex items-center gap-x-8 text-sm text-gray-600">
<div className="flex items-center gap-x-2">
@ -101,7 +101,7 @@ export function EducationalForParents() {
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{challenges.map((challenge, index) => (
<InfoCard
key={index}
key={index}
icon={challenge.icon}
title={challenge.title}
description={challenge.description}
@ -118,8 +118,8 @@ export function EducationalForParents() {
</h2>
<p className="mt-4 text-lg leading-8 text-gray-600">
Um processo simples e eficaz para transformar a leitura em uma aventura inesquecível.
</p>
</div>
</p>
</div>
<div className="mt-16 space-y-12">
{magicSteps.map((step, index) => (
<ProcessStep
@ -128,9 +128,9 @@ export function EducationalForParents() {
title={step.title}
description={step.description}
/>
))}
))}
</div>
</div>
</div>
{/* Comparison Section */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
@ -149,18 +149,18 @@ export function EducationalForParents() {
<p className="mt-4 text-lg leading-8 text-gray-600">
Descubra todas as vantagens que nossa plataforma oferece para o desenvolvimento do seu filho.
</p>
</div>
</div>
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{detailedBenefits.map((benefit, index) => (
<FeatureCard
key={index}
key={index}
icon={benefit.icon}
title={benefit.title}
description={benefit.description}
/>
))}
</div>
</div>
/>
))}
</div>
</div>
{/* Results Section */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
@ -171,7 +171,7 @@ export function EducationalForParents() {
<p className="mt-4 text-lg leading-8 text-gray-600">
Números que demonstram o impacto do Leiturama no aprendizado.
</p>
</div>
</div>
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<StatCard
icon={LineChart}
@ -222,12 +222,12 @@ export function EducationalForParents() {
/>
))}
</div>
</div>
</div>
{/* Pricing Section */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
<PlanForParents />
</div>
</div>
{/* FAQ Section */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24 bg-gradient-to-b from-purple-50">
@ -236,7 +236,7 @@ export function EducationalForParents() {
description="Encontre respostas para as perguntas mais comuns sobre nossa plataforma."
items={faqItems}
/>
</div>
</div>
{/* Final CTA */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
@ -371,7 +371,7 @@ const comparisonData = [
'Pais confiantes no desenvolvimento mágico'
]
}
];
];
const detailedBenefits = [
{

View File

@ -1,12 +0,0 @@
import React from 'react';
export function StudentDashboard(): JSX.Element {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">
Bem-vindo ao Dashboard
</h1>
{/* Conteúdo do dashboard será implementado aqui */}
</div>
);
}

View File

@ -1,71 +0,0 @@
import { AvatarUpload } from "@/components/ui/avatar-upload";
import { DatePicker } from "@/components/ui/date-picker";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@radix-ui/react-tabs";
export function StudentSettingsPage() {
return (
<div className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Configurações do Perfil
</h1>
{/* Seções em Tabs */}
<Tabs defaultValue="personal">
<TabsList>
<TabsTrigger value="personal">Informações Pessoais</TabsTrigger>
<TabsTrigger value="preferences">Preferências</TabsTrigger>
<TabsTrigger value="accessibility">Acessibilidade</TabsTrigger>
<TabsTrigger value="notifications">Notificações</TabsTrigger>
</TabsList>
<TabsContent value="personal">
<div className="space-y-6">
{/* Avatar Upload */}
<div className="flex items-center gap-4">
<AvatarUpload />
<div>
<h3 className="font-medium">Foto do Perfil</h3>
<p className="text-sm text-gray-500">
JPG ou PNG, máximo 2MB
</p>
</div>
</div>
{/* Informações Básicas */}
<div className="grid grid-cols-2 gap-4">
<Input
label="Nome Completo"
name="fullName"
placeholder="Seu nome completo"
/>
<Input
label="Nome Social/Apelido"
name="nickname"
placeholder="Como prefere ser chamado"
/>
<DatePicker
label="Data de Nascimento"
name="birthDate"
/>
<Select
label="Gênero"
name="gender"
options={[
{ value: 'male', label: 'Masculino' },
{ value: 'female', label: 'Feminino' },
{ value: 'non_binary', label: 'Não-binário' },
{ value: 'other', label: 'Outro' },
{ value: 'prefer_not_to_say', label: 'Prefiro não dizer' }
]}
/>
</div>
</div>
</TabsContent>
{/* Outras tabs... */}
</Tabs>
</div>
);
}

View File

@ -1,9 +1,9 @@
import { createBrowserRouter } from 'react-router-dom';
import { BaseLayout } from './components/layouts/BaseLayout';
import { HomePage } from './components/home/HomePage';
import { LoginForm } from './components/auth/LoginForm';
import { SchoolRegistrationForm } from './components/auth/SchoolRegistrationForm';
import { RegistrationForm } from './components/RegistrationForm';
import { StoryViewer } from './components/StoryViewer';
import { AuthCallback } from './pages/AuthCallback';
import { DashboardLayout } from './pages/dashboard/DashboardLayout';
import { DashboardHome } from './pages/dashboard/DashboardHome';
@ -32,12 +32,10 @@ import { TestWordHighlighter } from './pages/TestWordHighlighter';
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
import { EvidenceBased } from './pages/landing/EvidenceBased';
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
import { PageTracker } from './components/analytics/PageTracker';
function RootLayout({ children }: { children: React.ReactNode }) {
return (
<>
<PageTracker />
{children}
</>
);
@ -46,181 +44,187 @@ function RootLayout({ children }: { children: React.ReactNode }) {
export const router = createBrowserRouter([
{
path: '/',
element: <RootLayout><HomePage /></RootLayout>,
},
{
path: '/teste',
element: <TestWordHighlighter />,
},
{
path: '/para-pais',
element: <ParentsLandingPage />,
},
{
path: '/evidencias',
element: <EvidenceBased />,
},
{
path: '/evidencias/tsl',
element: <TextSalesLetter />,
},
{
path: '/login',
element: <BaseLayout />,
children: [
{
path: 'school',
element: <LoginForm userType="school" />,
path: '/',
element: <RootLayout><HomePage /></RootLayout>,
},
{
path: 'teacher',
element: <LoginForm userType="teacher" />,
path: '/teste',
element: <TestWordHighlighter />,
},
{
path: 'student',
element: <LoginForm userType="student" />,
}
]
},
{
path: '/register',
children: [
{
path: 'school',
element: <SchoolRegistrationForm />,
path: '/para-pais',
element: <ParentsLandingPage />,
},
{
path: 'teacher',
element: <RegistrationForm
userType="teacher"
onComplete={(userData) => {
console.log('Registro completo:', userData);
}}
/>,
}
]
},
{
path: '/dashboard',
element: (
<ProtectedRoute allowedRoles={['school']}>
<DashboardLayout />
</ProtectedRoute>
),
children: [
{
index: true,
element: <DashboardHome />,
path: '/evidencias',
element: <EvidenceBased />,
},
{
path: 'turmas',
path: '/evidencias/tsl',
element: <TextSalesLetter />,
},
{
path: '/login',
children: [
{
index: true,
element: <ClassesPage />,
path: 'school',
element: <LoginForm userType="school" />,
},
{
path: 'nova',
element: <CreateClassPage />,
path: 'teacher',
element: <LoginForm userType="teacher" />,
},
{
path: 'student',
element: <LoginForm userType="student" />,
}
]
},
{
path: 'professores',
path: '/register',
children: [
{
index: true,
element: <TeachersPage />,
path: 'school',
element: <SchoolRegistrationForm />,
},
{
path: 'convidar',
element: <InviteTeacherPage />,
path: 'teacher',
element: <RegistrationForm
userType="teacher"
onComplete={(userData) => {
console.log('Registro completo:', userData);
}}
/>,
}
]
},
{
path: 'alunos',
path: '/dashboard',
element: (
<ProtectedRoute allowedRoles={['school']}>
<DashboardLayout />
</ProtectedRoute>
),
children: [
{
index: true,
element: <StudentsPage />,
element: <DashboardHome />,
},
{
path: 'novo',
element: <AddStudentPage />,
path: 'turmas',
children: [
{
index: true,
element: <ClassesPage />,
},
{
path: 'nova',
element: <CreateClassPage />,
}
]
},
{
path: 'professores',
children: [
{
index: true,
element: <TeachersPage />,
},
{
path: 'convidar',
element: <InviteTeacherPage />,
}
]
},
{
path: 'alunos',
children: [
{
index: true,
element: <StudentsPage />,
},
{
path: 'novo',
element: <AddStudentPage />,
}
]
},
{
path: 'configuracoes',
element: <SettingsPage />
}
]
},
{
path: 'configuracoes',
element: <SettingsPage />
path: '/demo',
element: <StoryPageDemo />
},
{
path: '/auth/callback',
element: <AuthCallback />
},
{
path: '/aluno',
element: (
<ProtectedRoute allowedRoles={['student']}>
<StudentDashboardLayout />
</ProtectedRoute>
),
children: [
{
index: true,
element: <StudentDashboardPage />,
},
{
path: 'historias',
children: [
{
index: true,
element: <StudentStoriesPage />,
},
{
path: 'nova',
element: <CreateStoryPage />,
},
{
path: ':id',
element: <StoryPage />,
}
]
},
{
path: 'configuracoes',
element: <StudentSettingsPage />,
},
{
path: 'conquistas',
element: <AchievementsPage />,
},
{
path: 'turmas/:classId',
element: <StudentClassPage />,
}
]
},
{
path: '/admin/users',
element: (
<ProtectedRoute allowedRoles={['admin']}>
<UserManagementPage />
</ProtectedRoute>
),
},
{
path: '/para-educadores',
element: <EducationalForParents />,
},
{
path: '/aluno/historias/:id/exercicios/:type',
element: <ExercisePage />
}
]
},
{
path: '/demo',
element: <StoryPageDemo />
},
{
path: '/auth/callback',
element: <AuthCallback />
},
{
path: '/aluno',
element: (
<ProtectedRoute allowedRoles={['student']}>
<StudentDashboardLayout />
</ProtectedRoute>
),
children: [
{
index: true,
element: <StudentDashboardPage />,
},
{
path: 'historias',
children: [
{
index: true,
element: <StudentStoriesPage />,
},
{
path: 'nova',
element: <CreateStoryPage />,
},
{
path: ':id',
element: <StoryPage />,
}
]
},
{
path: 'configuracoes',
element: <StudentSettingsPage />,
},
{
path: 'conquistas',
element: <AchievementsPage />,
},
{
path: 'turmas/:classId',
element: <StudentClassPage />,
}
]
},
{
path: '/admin/users',
element: (
<ProtectedRoute allowedRoles={['admin']}>
<UserManagementPage />
</ProtectedRoute>
),
},
{
path: '/para-educadores',
element: <EducationalForParents />,
},
{
path: '/aluno/historias/:id/exercicios/:type',
element: <ExercisePage />
}
]);