mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
Compare commits
5 Commits
41a225d460
...
e1a99f32f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1a99f32f5 | ||
|
|
18cf6a2495 | ||
|
|
6a1a471ce5 | ||
|
|
bcbdd07a41 | ||
|
|
98411b2aa1 |
@ -12,6 +12,8 @@ import { AuthProvider } from './contexts/AuthContext'
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { Toaster } from './components/ui/toaster';
|
import { Toaster } from './components/ui/toaster';
|
||||||
|
import { router } from './routes';
|
||||||
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
type AppStep =
|
type AppStep =
|
||||||
| 'welcome'
|
| 'welcome'
|
||||||
@ -88,6 +90,7 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
||||||
{step === 'welcome' && (
|
{step === 'welcome' && (
|
||||||
<WelcomePage
|
<WelcomePage
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useRudderstack } from '../../hooks/useRudderstack';
|
import { useRudderstack } from '../../hooks/useRudderstack';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
@ -7,82 +7,106 @@ export function PageTracker() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { page } = useRudderstack();
|
const { page } = useRudderstack();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const lastPageTracked = useRef<string | null>(null);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Coleta informações do dispositivo/navegador
|
// Se já rastreamos esta página, não rastrear novamente
|
||||||
const deviceInfo = {
|
if (lastPageTracked.current === location.pathname) {
|
||||||
screenWidth: window.screen.width,
|
return;
|
||||||
screenHeight: window.screen.height,
|
}
|
||||||
viewportWidth: window.innerWidth,
|
|
||||||
viewportHeight: window.innerHeight,
|
|
||||||
deviceType: getDeviceType(),
|
|
||||||
deviceOrientation: window.screen.orientation.type,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
language: navigator.language,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Coleta informações de performance
|
// Limpa o timeout anterior se existir
|
||||||
const performanceInfo = {
|
if (timeoutRef.current) {
|
||||||
loadTime: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
|
clearTimeout(timeoutRef.current);
|
||||||
domInteractive: window.performance.timing.domInteractive - window.performance.timing.navigationStart,
|
}
|
||||||
firstContentfulPaint: getFirstContentfulPaint(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Informações da sessão
|
// Debounce de 300ms para evitar múltiplos eventos
|
||||||
const sessionInfo = {
|
timeoutRef.current = setTimeout(() => {
|
||||||
sessionStartTime: sessionStorage.getItem('sessionStartTime') || new Date().toISOString(),
|
// Coleta informações do dispositivo/navegador
|
||||||
isFirstVisit: !localStorage.getItem('returningVisitor'),
|
const deviceInfo = {
|
||||||
lastVisitedPage: sessionStorage.getItem('lastVisitedPage'),
|
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)
|
// Coleta informações de performance
|
||||||
const userTraits = user ? {
|
const performanceInfo = {
|
||||||
user_id: user.id,
|
loadTime: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
|
||||||
email: user.email,
|
domInteractive: window.performance.timing.domInteractive - window.performance.timing.navigationStart,
|
||||||
school_id: user.user_metadata?.school_id,
|
firstContentfulPaint: getFirstContentfulPaint(),
|
||||||
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
|
// Informações da sessão
|
||||||
...sessionInfo,
|
const sessionInfo = {
|
||||||
|
sessionStartTime: sessionStorage.getItem('sessionStartTime') || new Date().toISOString(),
|
||||||
// Traits do usuário
|
isFirstVisit: !localStorage.getItem('returningVisitor'),
|
||||||
...userTraits,
|
lastVisitedPage: sessionStorage.getItem('lastVisitedPage'),
|
||||||
|
};
|
||||||
// Metadados adicionais
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Atualiza informações da sessão
|
// Traits do usuário (se autenticado)
|
||||||
sessionStorage.setItem('lastVisitedPage', location.pathname);
|
const userTraits = user ? {
|
||||||
if (!localStorage.getItem('returningVisitor')) {
|
user_id: user.id,
|
||||||
localStorage.setItem('returningVisitor', 'true');
|
email: user.email,
|
||||||
}
|
school_id: user.user_metadata?.school_id,
|
||||||
if (!sessionStorage.getItem('sessionStartTime')) {
|
class_id: user.user_metadata?.class_id,
|
||||||
sessionStorage.setItem('sessionStartTime', new Date().toISOString());
|
name: user.user_metadata?.name,
|
||||||
}
|
role: user.user_metadata?.role,
|
||||||
}, [location, page, user]);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
330
src/components/analytics/README.md
Normal file
330
src/components/analytics/README.md
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
11
src/components/layouts/BaseLayout.tsx
Normal file
11
src/components/layouts/BaseLayout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { PageTracker } from '../analytics/PageTracker';
|
||||||
|
|
||||||
|
export function BaseLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTracker />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -134,7 +134,7 @@ export const EVENT_PROPERTIES = {
|
|||||||
STARTED_AT: 'started_at',
|
STARTED_AT: 'started_at',
|
||||||
COMPLETED_AT: 'completed_at',
|
COMPLETED_AT: 'completed_at',
|
||||||
DURATION: 'duration',
|
DURATION: 'duration',
|
||||||
TIMESTAMP: 'timestamp',
|
EVENT_TIMESTAMP: 'event_timestamp',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Propriedades de Formulário
|
// Propriedades de Formulário
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export function useButtonTracking(options: ButtonTrackingOptions = {}) {
|
|||||||
button_id: buttonId,
|
button_id: buttonId,
|
||||||
category,
|
category,
|
||||||
location,
|
location,
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
...properties,
|
...properties,
|
||||||
// Informações da página
|
// Informações da página
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
|
|||||||
@ -44,7 +44,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
// Informações do ambiente
|
// Informações do ambiente
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
user_agent: navigator.userAgent,
|
user_agent: navigator.userAgent,
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,7 +70,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
component_stack: componentStack,
|
component_stack: componentStack,
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
user_agent: navigator.userAgent,
|
user_agent: navigator.userAgent,
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,7 +99,6 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
status_code: error.status || error.statusCode,
|
status_code: error.status || error.statusCode,
|
||||||
request_data: requestData,
|
request_data: requestData,
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -50,35 +50,30 @@ export function useStudentTracking() {
|
|||||||
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
|
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
|
||||||
analytics.track('story_generated', {
|
analytics.track('story_generated', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackAudioRecorded = (properties: AudioRecordedProps) => {
|
const trackAudioRecorded = (properties: AudioRecordedProps) => {
|
||||||
analytics.track('audio_recorded', {
|
analytics.track('audio_recorded', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
|
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
|
||||||
analytics.track('exercise_completed', {
|
analytics.track('exercise_completed', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackInterestAdded = (properties: InterestActionProps) => {
|
const trackInterestAdded = (properties: InterestActionProps) => {
|
||||||
analytics.track('interest_added', {
|
analytics.track('interest_added', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackInterestRemoved = (properties: InterestActionProps) => {
|
const trackInterestRemoved = (properties: InterestActionProps) => {
|
||||||
analytics.track('interest_removed', {
|
analytics.track('interest_removed', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -47,58 +47,68 @@ export class Analytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inicializa o Rudderstack
|
// Inicializa o Rudderstack
|
||||||
init(options?: { writeKey?: string; dataPlaneUrl?: string }): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initPromise) return this.initPromise;
|
if (this.initPromise) return this.initPromise;
|
||||||
|
|
||||||
this.initPromise = new Promise((resolve, reject) => {
|
this.initPromise = new Promise<void>((resolve, reject) => {
|
||||||
const writeKey = options?.writeKey || this.writeKey || import.meta.env.VITE_RUDDERSTACK_WRITE_KEY;
|
const initializeAnalytics = async () => {
|
||||||
const dataPlaneUrl = options?.dataPlaneUrl || this.dataPlaneUrl || import.meta.env.VITE_RUDDERSTACK_DATA_PLANE_URL;
|
try {
|
||||||
|
const writeKey = this.writeKey;
|
||||||
if (!writeKey || !dataPlaneUrl) {
|
const dataPlaneUrl = this.dataPlaneUrl;
|
||||||
const error = new Error('Missing Rudderstack configuration');
|
|
||||||
this.handleError('init', error);
|
if (this.debug) {
|
||||||
reject(error);
|
console.log('Inicializando analytics com:', {
|
||||||
return;
|
writeKey,
|
||||||
}
|
dataPlaneUrl,
|
||||||
|
debug: this.debug
|
||||||
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',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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);
|
this.handleError('init', error);
|
||||||
reject(error);
|
reject(error);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.head.appendChild(script);
|
initializeAnalytics();
|
||||||
} catch (error) {
|
|
||||||
this.handleError('init', error);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.initPromise;
|
return this.initPromise;
|
||||||
@ -210,7 +220,7 @@ export class Analytics {
|
|||||||
// Enriquece as propriedades com dados padrão
|
// Enriquece as propriedades com dados padrão
|
||||||
private enrichEventProperties(properties?: Record<string, unknown>): BaseEventProperties {
|
private enrichEventProperties(properties?: Record<string, unknown>): BaseEventProperties {
|
||||||
const baseProperties: 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.URL]: window.location.href,
|
||||||
[EVENT_PROPERTIES.PAGE.TITLE]: document.title,
|
[EVENT_PROPERTIES.PAGE.TITLE]: document.title,
|
||||||
[EVENT_PROPERTIES.PAGE.PATH]: window.location.pathname,
|
[EVENT_PROPERTIES.PAGE.PATH]: window.location.pathname,
|
||||||
@ -248,5 +258,7 @@ export class Analytics {
|
|||||||
|
|
||||||
// Instância global
|
// Instância global
|
||||||
export const analytics = new Analytics({
|
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
|
||||||
});
|
});
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,6 +3,8 @@ import { ArrowLeft, ArrowRight, Mic, Volume2, Share2, ChevronDown, ChevronUp, Lo
|
|||||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||||
import { StoryMetrics } from '../../components/story/StoryMetrics';
|
import { StoryMetrics } from '../../components/story/StoryMetrics';
|
||||||
import type { StoryRecording } from '../../types/database';
|
import type { StoryRecording } from '../../types/database';
|
||||||
|
import { analytics } from '../../lib/analytics';
|
||||||
|
|
||||||
|
|
||||||
// Separar dados mock em arquivo próprio
|
// Separar dados mock em arquivo próprio
|
||||||
const DEMO_DATA = {
|
const DEMO_DATA = {
|
||||||
@ -126,6 +128,20 @@ export function StoryPageDemo(): JSX.Element {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRecording(false);
|
setIsRecording(false);
|
||||||
setShowMetrics(true);
|
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"
|
}, 3000); // Simula 3 segundos de "processamento"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Brain,
|
Brain,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@ -41,28 +41,28 @@ export function EducationalForParents() {
|
|||||||
<div className="absolute right-4 top-4 text-gray-600 flex items-center gap-2">
|
<div className="absolute right-4 top-4 text-gray-600 flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span className="text-sm">Tempo de leitura: 5 minutos</span>
|
<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">
|
<h1 className="mt-10 text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
||||||
Transforme o<br />
|
Transforme o<br />
|
||||||
Aprendizado em<br />
|
Aprendizado em<br />
|
||||||
Uma <span className="text-purple-600">Aventura Mágica</span>
|
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">
|
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-xl">
|
||||||
Histórias educativas personalizadas que encantam e ensinam,
|
Histórias educativas personalizadas que encantam e ensinam,
|
||||||
criadas especialmente para o desenvolvimento único do seu filho.
|
criadas especialmente para o desenvolvimento único do seu filho.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = '/cadastro'}
|
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"
|
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" />
|
<ArrowRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 flex items-center gap-x-8 text-sm text-gray-600">
|
<div className="mt-10 flex items-center gap-x-8 text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-x-2">
|
<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">
|
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{challenges.map((challenge, index) => (
|
{challenges.map((challenge, index) => (
|
||||||
<InfoCard
|
<InfoCard
|
||||||
key={index}
|
key={index}
|
||||||
icon={challenge.icon}
|
icon={challenge.icon}
|
||||||
title={challenge.title}
|
title={challenge.title}
|
||||||
description={challenge.description}
|
description={challenge.description}
|
||||||
@ -118,8 +118,8 @@ export function EducationalForParents() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg leading-8 text-gray-600">
|
<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.
|
Um processo simples e eficaz para transformar a leitura em uma aventura inesquecível.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-16 space-y-12">
|
<div className="mt-16 space-y-12">
|
||||||
{magicSteps.map((step, index) => (
|
{magicSteps.map((step, index) => (
|
||||||
<ProcessStep
|
<ProcessStep
|
||||||
@ -128,9 +128,9 @@ export function EducationalForParents() {
|
|||||||
title={step.title}
|
title={step.title}
|
||||||
description={step.description}
|
description={step.description}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Comparison Section */}
|
{/* Comparison Section */}
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
|
<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">
|
<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.
|
Descubra todas as vantagens que nossa plataforma oferece para o desenvolvimento do seu filho.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{detailedBenefits.map((benefit, index) => (
|
{detailedBenefits.map((benefit, index) => (
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
key={index}
|
key={index}
|
||||||
icon={benefit.icon}
|
icon={benefit.icon}
|
||||||
title={benefit.title}
|
title={benefit.title}
|
||||||
description={benefit.description}
|
description={benefit.description}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results Section */}
|
{/* Results Section */}
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
|
<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">
|
<p className="mt-4 text-lg leading-8 text-gray-600">
|
||||||
Números que demonstram o impacto do Leiturama no aprendizado.
|
Números que demonstram o impacto do Leiturama no aprendizado.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={LineChart}
|
icon={LineChart}
|
||||||
@ -222,12 +222,12 @@ export function EducationalForParents() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pricing Section */}
|
{/* Pricing Section */}
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
|
||||||
<PlanForParents />
|
<PlanForParents />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* 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">
|
<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."
|
description="Encontre respostas para as perguntas mais comuns sobre nossa plataforma."
|
||||||
items={faqItems}
|
items={faqItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Final CTA */}
|
{/* Final CTA */}
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-24">
|
<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'
|
'Pais confiantes no desenvolvimento mágico'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const detailedBenefits = [
|
const detailedBenefits = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
288
src/routes.tsx
288
src/routes.tsx
@ -1,9 +1,9 @@
|
|||||||
import { createBrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter } from 'react-router-dom';
|
||||||
|
import { BaseLayout } from './components/layouts/BaseLayout';
|
||||||
import { HomePage } from './components/home/HomePage';
|
import { HomePage } from './components/home/HomePage';
|
||||||
import { LoginForm } from './components/auth/LoginForm';
|
import { LoginForm } from './components/auth/LoginForm';
|
||||||
import { SchoolRegistrationForm } from './components/auth/SchoolRegistrationForm';
|
import { SchoolRegistrationForm } from './components/auth/SchoolRegistrationForm';
|
||||||
import { RegistrationForm } from './components/RegistrationForm';
|
import { RegistrationForm } from './components/RegistrationForm';
|
||||||
import { StoryViewer } from './components/StoryViewer';
|
|
||||||
import { AuthCallback } from './pages/AuthCallback';
|
import { AuthCallback } from './pages/AuthCallback';
|
||||||
import { DashboardLayout } from './pages/dashboard/DashboardLayout';
|
import { DashboardLayout } from './pages/dashboard/DashboardLayout';
|
||||||
import { DashboardHome } from './pages/dashboard/DashboardHome';
|
import { DashboardHome } from './pages/dashboard/DashboardHome';
|
||||||
@ -32,12 +32,10 @@ import { TestWordHighlighter } from './pages/TestWordHighlighter';
|
|||||||
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
|
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
|
||||||
import { EvidenceBased } from './pages/landing/EvidenceBased';
|
import { EvidenceBased } from './pages/landing/EvidenceBased';
|
||||||
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
||||||
import { PageTracker } from './components/analytics/PageTracker';
|
|
||||||
|
|
||||||
function RootLayout({ children }: { children: React.ReactNode }) {
|
function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTracker />
|
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -46,181 +44,187 @@ function RootLayout({ children }: { children: React.ReactNode }) {
|
|||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <RootLayout><HomePage /></RootLayout>,
|
element: <BaseLayout />,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/teste',
|
|
||||||
element: <TestWordHighlighter />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/para-pais',
|
|
||||||
element: <ParentsLandingPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/evidencias',
|
|
||||||
element: <EvidenceBased />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/evidencias/tsl',
|
|
||||||
element: <TextSalesLetter />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'school',
|
path: '/',
|
||||||
element: <LoginForm userType="school" />,
|
element: <RootLayout><HomePage /></RootLayout>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'teacher',
|
path: '/teste',
|
||||||
element: <LoginForm userType="teacher" />,
|
element: <TestWordHighlighter />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'student',
|
path: '/para-pais',
|
||||||
element: <LoginForm userType="student" />,
|
element: <ParentsLandingPage />,
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/register',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'school',
|
|
||||||
element: <SchoolRegistrationForm />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'teacher',
|
path: '/evidencias',
|
||||||
element: <RegistrationForm
|
element: <EvidenceBased />,
|
||||||
userType="teacher"
|
|
||||||
onComplete={(userData) => {
|
|
||||||
console.log('Registro completo:', userData);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/dashboard',
|
|
||||||
element: (
|
|
||||||
<ProtectedRoute allowedRoles={['school']}>
|
|
||||||
<DashboardLayout />
|
|
||||||
</ProtectedRoute>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <DashboardHome />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'turmas',
|
path: '/evidencias/tsl',
|
||||||
|
element: <TextSalesLetter />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: 'school',
|
||||||
element: <ClassesPage />,
|
element: <LoginForm userType="school" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'nova',
|
path: 'teacher',
|
||||||
element: <CreateClassPage />,
|
element: <LoginForm userType="teacher" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'student',
|
||||||
|
element: <LoginForm userType="student" />,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'professores',
|
path: '/register',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: 'school',
|
||||||
element: <TeachersPage />,
|
element: <SchoolRegistrationForm />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'convidar',
|
path: 'teacher',
|
||||||
element: <InviteTeacherPage />,
|
element: <RegistrationForm
|
||||||
|
userType="teacher"
|
||||||
|
onComplete={(userData) => {
|
||||||
|
console.log('Registro completo:', userData);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'alunos',
|
path: '/dashboard',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute allowedRoles={['school']}>
|
||||||
|
<DashboardLayout />
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <StudentsPage />,
|
element: <DashboardHome />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'novo',
|
path: 'turmas',
|
||||||
element: <AddStudentPage />,
|
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',
|
path: '/demo',
|
||||||
element: <SettingsPage />
|
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 />
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
Loading…
Reference in New Issue
Block a user