Correcoes

This commit is contained in:
Lucas Santana 2024-12-19 19:36:07 -03:00
parent 26888e9824
commit 677ee422c4
17 changed files with 2312 additions and 0 deletions

23
.eslintrc.json Normal file
View File

@ -0,0 +1,23 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"ignorePatterns": ["dist", ".eslintrc.json"],
"parser": "@typescript-eslint/parser",
"plugins": ["react-refresh"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
],
"@typescript-eslint/no-unused-vars": "warn",
"no-unused-vars": "warn"
}
}

4
public/book.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -0,0 +1,263 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { School, Eye, EyeOff } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import { supabase } from '../../lib/supabase';
export function SchoolRegistrationForm() {
const navigate = useNavigate();
const { error: authError } = useAuth();
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [formData, setFormData] = useState({
schoolName: '',
email: '',
password: '',
confirmPassword: '',
directorName: '',
});
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
if (formData.password !== formData.confirmPassword) {
setError('As senhas não coincidem');
setLoading(false);
return;
}
try {
// 1. Criar usuário na autenticação
const { data: authData, error: signUpError } = await supabase.auth.signUp({
email: formData.email,
password: formData.password,
options: {
data: {
role: 'school_admin',
school_name: formData.schoolName,
director_name: formData.directorName
},
emailRedirectTo: `${window.location.origin}/auth/callback`
}
});
if (signUpError) {
console.error('Erro no signup:', signUpError);
throw new Error(signUpError.message);
}
if (!authData.user?.id) {
throw new Error('Usuário não foi criado corretamente');
}
// 2. Criar registro da escola usando a função do servidor
const { error: schoolError } = await supabase.rpc('create_school', {
school_id: authData.user.id,
school_name: formData.schoolName,
school_email: formData.email,
director_name: formData.directorName
});
if (schoolError) {
console.error('Erro ao criar escola:', schoolError);
throw new Error(schoolError.message);
}
// 3. Redirecionar para login com mensagem de sucesso
navigate('/login/school', {
state: {
message: 'Cadastro realizado com sucesso! Por favor, verifique seu email para confirmar o cadastro.'
}
});
} catch (err) {
console.error('Erro detalhado:', err);
setError(
err instanceof Error
? err.message
: 'Erro ao cadastrar escola. Por favor, tente novamente.'
);
} finally {
setLoading(false);
}
};
const togglePasswordVisibility = (field: 'password' | 'confirmPassword') => {
if (field === 'password') {
setShowPassword(!showPassword);
} else {
setShowConfirmPassword(!showConfirmPassword);
}
};
return (
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto bg-white rounded-xl shadow-md p-8">
<div className="text-center mb-8">
<div className="flex justify-center">
<School className="h-12 w-12 text-purple-600" />
</div>
<h2 className="mt-4 text-3xl font-bold text-gray-900">
Cadastro de Escola
</h2>
<p className="mt-2 text-gray-600">
Comece a transformar a educação com histórias interativas
</p>
</div>
{(error || authError) && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg text-sm">
{error || authError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="schoolName" className="block text-sm font-medium text-gray-700">
Nome da Escola
</label>
<input
id="schoolName"
name="schoolName"
type="text"
required
value={formData.schoolName}
onChange={handleChange}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email Institucional
</label>
<input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleChange}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="relative">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Senha
</label>
<div className="relative">
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
required
value={formData.password}
onChange={handleChange}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 pr-10"
/>
<button
type="button"
onClick={() => togglePasswordVisibility('password')}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<div className="relative">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
Confirmar Senha
</label>
<div className="relative">
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
required
value={formData.confirmPassword}
onChange={handleChange}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 pr-10"
/>
<button
type="button"
onClick={() => togglePasswordVisibility('confirmPassword')}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
</div>
<div>
<label htmlFor="directorName" className="block text-sm font-medium text-gray-700">
Nome do Diretor(a)
</label>
<input
id="directorName"
name="directorName"
type="text"
required
value={formData.directorName}
onChange={handleChange}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
<div className="flex items-center">
<input
id="terms"
name="terms"
type="checkbox"
required
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
/>
<label htmlFor="terms" className="ml-2 block text-sm text-gray-900">
Li e aceito os{' '}
<a href="#" className="text-purple-600 hover:text-purple-500">
termos de uso
</a>{' '}
e a{' '}
<a href="#" className="text-purple-600 hover:text-purple-500">
política de privacidade
</a>
</label>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-lg font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
>
{loading ? 'Cadastrando...' : 'Cadastrar Escola'}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
tem uma conta?{' '}
<button
onClick={() => navigate('/login/school')}
className="text-purple-600 hover:text-purple-500 font-medium"
>
Faça login
</button>
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,275 @@
import React, { useState } from 'react';
import { BookOpen, Users, Shield, Sparkles, ArrowRight, School, BookCheck } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export function HomePage() {
const navigate = useNavigate();
const [showUserOptions, setShowUserOptions] = useState(false);
// Funções de navegação
const handleLoginClick = () => {
setShowUserOptions(true);
};
const handleSchoolLogin = () => {
navigate('/login/school');
};
const handleTeacherLogin = () => {
navigate('/login/teacher');
};
const handleStudentLogin = () => {
navigate('/login/student');
};
const handleSchoolRegister = () => {
navigate('/register/school');
};
const handleDemo = () => {
navigate('/demo');
};
return (
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white">
{/* Header/Nav */}
<nav className="bg-white/80 backdrop-blur-md fixed w-full z-50 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<BookOpen className="h-8 w-8 text-purple-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Histórias Mágicas</span>
</div>
<div className="flex items-center gap-4">
<div className="relative">
<button
onClick={handleLoginClick}
className="text-gray-600 hover:text-gray-900 px-3 py-2"
>
Entrar
</button>
{showUserOptions && (
<div className="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="py-1" role="menu">
<button
onClick={handleSchoolLogin}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-purple-50"
>
Entrar como Escola
</button>
<button
onClick={handleTeacherLogin}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-purple-50"
>
Entrar como Professor
</button>
<button
onClick={handleStudentLogin}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-purple-50"
>
Entrar como Aluno
</button>
</div>
</div>
)}
</div>
<button
onClick={handleSchoolRegister}
className="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition"
>
Cadastrar Escola
</button>
</div>
</div>
</div>
</nav>
{/* Hero Section */}
<div className="pt-32 pb-20 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-gray-900 mb-6">
Transforme a Educação com
<span className="text-purple-600"> Histórias Interativas</span>
</h1>
<p className="text-xl text-gray-600 mb-8 max-w-3xl mx-auto">
Uma plataforma educacional que conecta escolas, professores e alunos através
de histórias personalizadas e experiências de aprendizado únicas.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={handleSchoolRegister}
className="bg-purple-600 text-white px-8 py-4 rounded-xl hover:bg-purple-700 transition flex items-center justify-center gap-2 text-lg font-semibold"
>
Cadastrar sua Escola
<ArrowRight className="w-5 h-5" />
</button>
<button
onClick={handleDemo}
className="border-2 border-purple-600 text-purple-600 px-8 py-4 rounded-xl hover:bg-purple-50 transition text-lg font-semibold"
>
Ver Demonstração
</button>
</div>
</div>
</div>
</div>
{/* Features Grid */}
<div className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Tudo que você precisa em um lugar
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Uma plataforma completa para criar, gerenciar e compartilhar histórias educativas
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<FeatureCard
icon={<School />}
title="Gestão Escolar"
description="Gerencie turmas, professores e alunos de forma simples e eficiente"
/>
<FeatureCard
icon={<BookOpen />}
title="Histórias Interativas"
description="Crie e compartilhe histórias educativas personalizadas"
/>
<FeatureCard
icon={<Users />}
title="Colaboração"
description="Trabalhe em conjunto com outros professores e compartilhe recursos"
/>
<FeatureCard
icon={<Shield />}
title="Ambiente Seguro"
description="Proteção de dados e conteúdo adequado para crianças"
/>
<FeatureCard
icon={<Sparkles />}
title="Personalização"
description="Adapte o conteúdo ao perfil e necessidades de cada aluno"
/>
<FeatureCard
icon={<BookCheck />}
title="Acompanhamento"
description="Monitore o progresso e engajamento dos alunos"
/>
</div>
</div>
</div>
{/* Stats Section */}
<div className="bg-purple-600 py-20 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
<StatCard number="1000+" label="Escolas" />
<StatCard number="5000+" label="Professores" />
<StatCard number="50000+" label="Histórias Criadas" />
</div>
</div>
</div>
{/* CTA Section */}
<div className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-2xl p-8 md:p-16 text-center text-white">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Comece sua jornada hoje
</h2>
<p className="text-lg mb-8 max-w-2xl mx-auto">
Transforme a educação em sua escola com histórias interativas
</p>
<button
onClick={handleSchoolRegister}
className="bg-white text-purple-600 px-8 py-4 rounded-xl hover:bg-purple-50 transition text-lg font-semibold"
>
Cadastrar Escola Gratuitamente
</button>
</div>
</div>
</div>
{/* Footer */}
<footer className="bg-gray-50 border-t border-gray-200">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
<div>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">Produto</h3>
<ul className="mt-4 space-y-4">
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Recursos</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Preços</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Demonstração</a></li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">Suporte</h3>
<ul className="mt-4 space-y-4">
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Documentação</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Tutoriais</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">FAQ</a></li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">Empresa</h3>
<ul className="mt-4 space-y-4">
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Sobre</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Blog</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Carreiras</a></li>
</ul>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">Legal</h3>
<ul className="mt-4 space-y-4">
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Privacidade</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Termos</a></li>
<li><a href="#" className="text-base text-gray-600 hover:text-gray-900">Segurança</a></li>
</ul>
</div>
</div>
<div className="mt-8 border-t border-gray-200 pt-8 text-center">
<p className="text-base text-gray-400">
&copy; 2024 Histórias Mágicas. Todos os direitos reservados.
</p>
</div>
</div>
</footer>
</div>
);
}
interface FeatureCardProps {
icon: React.ReactNode;
title: string;
description: string;
}
function FeatureCard({ icon, title, description }: FeatureCardProps) {
return (
<div className="bg-gray-50 p-6 rounded-xl hover:shadow-lg transition">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center text-purple-600 mb-4">
{icon}
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
}
interface StatCardProps {
number: string;
label: string;
}
function StatCard({ number, label }: StatCardProps) {
return (
<div>
<div className="text-4xl font-bold mb-2">{number}</div>
<div className="text-purple-200">{label}</div>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../lib/supabase';
export function AuthCallback() {
const navigate = useNavigate();
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
navigate('/dashboard');
}
});
}, [navigate]);
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold text-gray-900">
Verificando autenticação...
</h2>
<p className="mt-2 text-gray-600">
Por favor, aguarde enquanto confirmamos seu acesso.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Users, GraduationCap, BookOpen } from 'lucide-react';
import { useDatabase } from '../../hooks/useDatabase';
export function DashboardHome() {
const { loading, error } = useDatabase();
const [stats, setStats] = React.useState({
totalClasses: 0,
totalTeachers: 0,
totalStudents: 0,
totalStories: 0
});
React.useEffect(() => {
// Implementar busca de estatísticas
}, []);
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-100 rounded-lg">
<Users className="h-6 w-6 text-purple-600" />
</div>
<div>
<p className="text-sm text-gray-600">Total de Turmas</p>
<p className="text-2xl font-semibold text-gray-900">
{loading ? '...' : stats.totalClasses}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<GraduationCap className="h-6 w-6 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-600">Total de Professores</p>
<p className="text-2xl font-semibold text-gray-900">
{loading ? '...' : stats.totalTeachers}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="flex items-center gap-4">
<div className="p-3 bg-green-100 rounded-lg">
<Users className="h-6 w-6 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-600">Total de Alunos</p>
<p className="text-2xl font-semibold text-gray-900">
{loading ? '...' : stats.totalStudents}
</p>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Últimas Turmas
</h2>
{/* Lista de últimas turmas */}
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Histórias Recentes
</h2>
{/* Lista de histórias recentes */}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,135 @@
import React from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
Users,
GraduationCap,
BookOpen,
Settings,
LogOut,
School,
UserRound
} from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
export function DashboardLayout() {
const navigate = useNavigate();
const { signOut } = useAuth();
const handleLogout = async () => {
await signOut();
navigate('/');
};
return (
<div className="min-h-screen bg-gray-50">
{/* Sidebar */}
<aside className="fixed left-0 top-0 h-full w-64 bg-white border-r border-gray-200">
<div className="flex items-center gap-2 p-6 border-b border-gray-200">
<School className="h-8 w-8 text-purple-600" />
<span className="font-semibold text-gray-900">Histórias Mágicas</span>
</div>
<nav className="p-4 space-y-1">
<NavLink
to="/dashboard"
end
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<LayoutDashboard className="h-5 w-5" />
Visão Geral
</NavLink>
<NavLink
to="/dashboard/turmas"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<Users className="h-5 w-5" />
Turmas
</NavLink>
<NavLink
to="/dashboard/professores"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<GraduationCap className="h-5 w-5" />
Professores
</NavLink>
<NavLink
to="/dashboard/alunos"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<UserRound className="h-5 w-5" />
Alunos
</NavLink>
<NavLink
to="/dashboard/historias"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<BookOpen className="h-5 w-5" />
Histórias
</NavLink>
<NavLink
to="/dashboard/configuracoes"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<Settings className="h-5 w-5" />
Configurações
</NavLink>
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50 w-full"
>
<LogOut className="h-5 w-5" />
Sair
</button>
</nav>
</aside>
{/* Main Content */}
<main className="ml-64 p-8">
<Outlet />
</main>
</div>
);
}

View File

@ -0,0 +1,119 @@
import React from 'react';
import { Plus, Search, MoreVertical } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useDatabase } from '../../../hooks/useDatabase';
interface Class {
id: string;
name: string;
grade: string;
year: number;
teacher_count: number;
student_count: number;
}
export function ClassesPage() {
const navigate = useNavigate();
const { loading, error } = useDatabase();
const [classes, setClasses] = React.useState<Class[]>([]);
const [searchTerm, setSearchTerm] = React.useState('');
React.useEffect(() => {
// Implementar busca de turmas
}, []);
const handleCreateClass = () => {
navigate('/dashboard/turmas/nova');
};
const handleClassClick = (classId: string) => {
navigate(`/dashboard/turmas/${classId}`);
};
const filteredClasses = classes.filter(c =>
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.grade.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Turmas</h1>
<button
onClick={handleCreateClass}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Nova Turma
</button>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
</div>
)}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar turmas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">Carregando...</div>
) : filteredClasses.length === 0 ? (
<div className="p-8 text-center text-gray-500">
Nenhuma turma encontrada
</div>
) : (
<div className="divide-y divide-gray-200">
{filteredClasses.map((classItem) => (
<div
key={classItem.id}
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => handleClassClick(classItem.id)}
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium text-gray-900">
{classItem.name}
</h3>
<p className="text-sm text-gray-500">
{classItem.grade} - {classItem.year}
</p>
</div>
<div className="flex items-center gap-6">
<div className="text-sm text-gray-500">
<span className="font-medium text-gray-900">
{classItem.teacher_count}
</span>{' '}
professores
</div>
<div className="text-sm text-gray-500">
<span className="font-medium text-gray-900">
{classItem.student_count}
</span>{' '}
alunos
</div>
<button className="p-2 hover:bg-gray-100 rounded-full">
<MoreVertical className="h-5 w-5 text-gray-400" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import { supabase } from '../../../lib/supabase';
import type { Class } from '../../../types';
export function CreateClassPage() {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
const [formData, setFormData] = useState<Omit<Class, 'id' | 'school_id' | 'created_at' | 'updated_at'>>({
name: '',
grade: '',
year: new Date().getFullYear(),
period: 'morning'
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setFormError(null);
try {
const { data: { session }, error: authError } = await supabase.auth.getSession();
if (authError) throw authError;
if (!session?.user?.id) {
throw new Error('Usuário não autenticado');
}
const { data: newClass, error: classError } = await supabase
.from('classes')
.insert({
school_id: session.user.id,
name: formData.name,
grade: formData.grade,
year: formData.year,
period: formData.period
})
.select()
.single();
if (classError) throw classError;
navigate('/dashboard/turmas', {
state: { message: 'Turma criada com sucesso!' }
});
} catch (err) {
console.error('Erro ao criar turma:', err);
setFormError(err instanceof Error ? err.message : 'Erro ao criar turma');
} finally {
setIsLoading(false);
}
};
return (
<div>
<button
onClick={() => navigate('/dashboard/turmas')}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
>
<ArrowLeft className="h-5 w-5" />
Voltar para turmas
</button>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Nova Turma</h1>
{formError && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{formError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nome da Turma
</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
placeholder="Ex: 5º Ano A"
/>
</div>
<div>
<label htmlFor="grade" className="block text-sm font-medium text-gray-700">
Série/Ano
</label>
<input
type="text"
id="grade"
value={formData.grade}
onChange={(e) => setFormData({ ...formData, grade: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
placeholder="Ex: 5º Ano"
/>
</div>
<div>
<label htmlFor="year" className="block text-sm font-medium text-gray-700">
Ano Letivo
</label>
<input
type="number"
id="year"
value={formData.year}
onChange={(e) => setFormData({ ...formData, year: parseInt(e.target.value) })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
min={2024}
max={2100}
/>
</div>
<div>
<label htmlFor="period" className="block text-sm font-medium text-gray-700">
Período
</label>
<select
id="period"
value={formData.period}
onChange={(e) => setFormData({ ...formData, period: e.target.value as Class['period'] })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
>
<option value="morning">Manhã</option>
<option value="afternoon">Tarde</option>
<option value="evening">Noite</option>
</select>
</div>
<div className="pt-4">
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
>
{isLoading ? 'Criando...' : 'Criar Turma'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,283 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import { useDatabase } from '../../../hooks/useDatabase';
import { supabase } from '../../../lib/supabase';
import { sendStudentCredentialsEmail } from '../../../services/email';
interface Class {
id: string;
name: string;
grade: string;
}
export function AddStudentPage() {
const navigate = useNavigate();
const { loading, error } = useDatabase();
const [classes, setClasses] = React.useState<Class[]>([]);
const [formData, setFormData] = React.useState({
name: '',
email: '',
class_id: '',
guardian_name: '',
guardian_email: '',
guardian_phone: '',
});
const [formError, setFormError] = React.useState<string | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
const fetchClasses = async () => {
try {
const { data: authData } = await supabase.auth.getSession();
if (!authData.session?.user) return;
const { data: schoolData, error: schoolError } = await supabase
.from('schools')
.select('id')
.eq('id', authData.session.user.id)
.single();
if (schoolError) throw schoolError;
const { data: classesData, error: classesError } = await supabase
.from('classes')
.select('id, name, grade')
.eq('school_id', schoolData.id)
.order('name');
if (classesError) throw classesError;
setClasses(classesData || []);
} catch (err) {
console.error('Erro ao buscar turmas:', err);
}
};
fetchClasses();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setFormError(null);
try {
const { data: authData } = await supabase.auth.getSession();
if (!authData.session?.user) {
throw new Error('Usuário não autenticado');
}
const { data: schoolData, error: schoolError } = await supabase
.from('schools')
.select('id')
.eq('id', authData.session.user.id)
.single();
if (schoolError) throw schoolError;
// Gerar senha temporária
const tempPassword = generateTempPassword();
// Criar usuário para o aluno
const { data: userData, error: userError } = await supabase.auth.signUp({
email: formData.email,
password: tempPassword,
options: {
data: {
role: 'student',
name: formData.name,
school_id: schoolData.id
}
}
});
if (userError) throw userError;
if (!userData.user) throw new Error('Erro ao criar usuário');
// Criar registro do aluno
const { data: newStudent, error: studentError } = await supabase
.from('students')
.insert({
id: userData.user.id,
school_id: schoolData.id,
class_id: formData.class_id,
name: formData.name,
email: formData.email,
guardian_name: formData.guardian_name,
guardian_email: formData.guardian_email,
guardian_phone: formData.guardian_phone
})
.select()
.single();
if (studentError) throw studentError;
// Enviar emails com as credenciais
const emailSent = await sendStudentCredentialsEmail({
studentName: formData.name,
studentEmail: formData.email,
password: tempPassword,
guardianName: formData.guardian_name,
guardianEmail: formData.guardian_email
});
navigate('/dashboard/alunos', {
state: {
message: `Aluno adicionado com sucesso! ${
emailSent
? 'As credenciais foram enviadas por email.'
: 'Não foi possível enviar os emails com as credenciais.'
}`
}
});
} catch (err) {
console.error('Erro ao adicionar aluno:', err);
setFormError(err instanceof Error ? err.message : 'Erro ao adicionar aluno');
} finally {
setIsLoading(false);
}
};
return (
<div>
<button
onClick={() => navigate('/dashboard/alunos')}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
>
<ArrowLeft className="h-5 w-5" />
Voltar para alunos
</button>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Adicionar Aluno</h1>
{formError && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{formError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nome do Aluno
</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email do Aluno
</label>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="class_id" className="block text-sm font-medium text-gray-700">
Turma
</label>
<select
id="class_id"
value={formData.class_id}
onChange={(e) => setFormData({ ...formData, class_id: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
>
<option value="">Selecione uma turma</option>
{classes.map((c) => (
<option key={c.id} value={c.id}>
{c.name} - {c.grade}
</option>
))}
</select>
</div>
<div className="border-t border-gray-200 pt-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Informações do Responsável
</h2>
<div className="space-y-6">
<div>
<label htmlFor="guardian_name" className="block text-sm font-medium text-gray-700">
Nome do Responsável
</label>
<input
type="text"
id="guardian_name"
value={formData.guardian_name}
onChange={(e) => setFormData({ ...formData, guardian_name: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="guardian_email" className="block text-sm font-medium text-gray-700">
Email do Responsável
</label>
<input
type="email"
id="guardian_email"
value={formData.guardian_email}
onChange={(e) => setFormData({ ...formData, guardian_email: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="guardian_phone" className="block text-sm font-medium text-gray-700">
Telefone do Responsável
</label>
<input
type="tel"
id="guardian_phone"
value={formData.guardian_phone}
onChange={(e) => setFormData({ ...formData, guardian_phone: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
</div>
</div>
<div className="pt-4">
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
>
{isLoading ? 'Adicionando...' : 'Adicionar Aluno'}
</button>
</div>
</form>
</div>
</div>
);
}
// Função auxiliar para gerar senha temporária
function generateTempPassword() {
const length = 12;
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}

View File

@ -0,0 +1,173 @@
import React from 'react';
import { Plus, Search, MoreVertical, GraduationCap } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useDatabase } from '../../../hooks/useDatabase';
import { supabase } from '../../../lib/supabase';
interface StudentData {
id: string;
name: string;
email: string;
class_id: string;
school_id: string;
classes: {
name: string;
};
}
interface StudentWithDetails {
id: string;
name: string;
email: string;
class_name: string;
stories_count: number;
status: 'active' | 'inactive';
}
export function StudentsPage() {
const navigate = useNavigate();
const { loading, error } = useDatabase();
const [students, setStudents] = React.useState<StudentWithDetails[]>([]);
const [searchTerm, setSearchTerm] = React.useState('');
React.useEffect(() => {
const fetchStudents = async () => {
try {
const { data: authData } = await supabase.auth.getSession();
if (!authData.session?.user) return;
const { data: studentsData, error: studentsError } = await supabase
.from('students')
.select(`
id,
name,
email,
class_id,
classes (
name
)
`);
if (studentsError) throw studentsError;
const studentsWithCounts = studentsData.map((student: StudentData) => ({
id: student.id,
name: student.name,
email: student.email,
class_name: student.classes?.name || 'Sem turma',
stories_count: 0,
status: 'active' as const
}));
setStudents(studentsWithCounts);
} catch (err) {
console.error('Erro ao buscar alunos:', err);
}
};
fetchStudents();
}, []);
const handleAddStudent = () => {
navigate('/dashboard/alunos/novo');
};
const handleStudentClick = (studentId: string) => {
navigate(`/dashboard/alunos/${studentId}`);
};
const getStatusColor = (status: StudentData['status']) => {
return status === 'active'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800';
};
const getStatusText = (status: StudentData['status']) => {
return status === 'active' ? 'Ativo' : 'Inativo';
};
const filteredStudents = students.filter(s =>
s.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
s.class_name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Alunos</h1>
<button
onClick={handleAddStudent}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Adicionar Aluno
</button>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
</div>
)}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar alunos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">Carregando...</div>
) : filteredStudents.length === 0 ? (
<div className="p-8 text-center text-gray-500">
Nenhum aluno encontrado
</div>
) : (
<div className="divide-y divide-gray-200">
{filteredStudents.map((student) => (
<div
key={student.id}
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => handleStudentClick(student.id)}
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium text-gray-900">
{student.name}
</h3>
<div className="flex items-center gap-2 text-sm text-gray-500">
<GraduationCap className="h-4 w-4" />
{student.class_name}
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-sm text-gray-500">
<span className="font-medium text-gray-900">
{student.stories_count}
</span>{' '}
histórias
</div>
<div className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(student.status)}`}>
{getStatusText(student.status)}
</div>
<button className="p-2 hover:bg-gray-100 rounded-full">
<MoreVertical className="h-5 w-5 text-gray-400" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,148 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Send } from 'lucide-react';
import { useDatabase } from '../../../hooks/useDatabase';
import { supabase } from '../../../lib/supabase';
export function InviteTeacherPage() {
const navigate = useNavigate();
const { inviteTeacher } = useDatabase();
const [isLoading, setIsLoading] = React.useState(false);
const [formData, setFormData] = React.useState({
name: '',
email: '',
subject: '',
message: ''
});
const [formError, setFormError] = React.useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setFormError(null);
try {
const { data: authData } = await supabase.auth.getSession();
if (!authData.session?.user) {
throw new Error('Usuário não autenticado');
}
const { data: schoolData, error: schoolError } = await supabase
.from('schools')
.select('id')
.eq('id', authData.session.user.id)
.single();
if (schoolError) throw schoolError;
const result = await inviteTeacher(schoolData.id, {
name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message
});
if (!result) {
throw new Error('Erro ao enviar convite');
}
navigate('/dashboard/professores', {
state: { message: 'Convite enviado com sucesso!' }
});
} catch (err) {
console.error('Erro ao enviar convite:', err);
setFormError(err instanceof Error ? err.message : 'Erro ao enviar convite');
} finally {
setIsLoading(false);
}
};
return (
<div>
<button
onClick={() => navigate('/dashboard/professores')}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
>
<ArrowLeft className="h-5 w-5" />
Voltar para professores
</button>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Convidar Professor</h1>
{formError && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{formError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nome do Professor
</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700">
Disciplina
</label>
<input
type="text"
id="subject"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
Mensagem Personalizada (opcional)
</label>
<textarea
id="message"
rows={4}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
<div className="pt-4">
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center items-center gap-2 py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
>
<Send className="h-4 w-4" />
{isLoading ? 'Enviando...' : 'Enviar Convite'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,191 @@
import React from 'react';
import { Plus, Search, MoreVertical, Mail } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useDatabase } from '../../../hooks/useDatabase';
import { supabase } from '../../../lib/supabase';
interface Teacher {
id: string;
name: string;
email: string;
subject?: string;
class_count: number;
status: 'active' | 'pending' | 'inactive';
}
export function TeachersPage() {
const navigate = useNavigate();
const { loading, error } = useDatabase();
const [teachers, setTeachers] = React.useState<Teacher[]>([]);
const [searchTerm, setSearchTerm] = React.useState('');
React.useEffect(() => {
const fetchTeachers = async () => {
try {
const { data: authData } = await supabase.auth.getSession();
if (!authData.session?.user) return;
const { data: schoolData, error: schoolError } = await supabase
.from('schools')
.select('id')
.eq('id', authData.session.user.id)
.single();
if (schoolError) throw schoolError;
const { data: teachersData, error: teachersError } = await supabase
.from('teachers')
.select(`
id,
name,
email,
subject,
status
`)
.eq('school_id', schoolData.id);
if (teachersError) throw teachersError;
// Buscar contagem de turmas para cada professor
const teachersWithCounts = await Promise.all(
teachersData.map(async (teacher) => {
const { data: countData } = await supabase
.rpc('get_teacher_class_count', { teacher_id: teacher.id });
return {
...teacher,
class_count: countData || 0
};
})
);
setTeachers(teachersWithCounts);
} catch (err) {
console.error('Erro ao buscar professores:', err);
}
};
fetchTeachers();
}, []);
const handleInviteTeacher = () => {
navigate('/dashboard/professores/convidar');
};
const handleTeacherClick = (teacherId: string) => {
navigate(`/dashboard/professores/${teacherId}`);
};
const getStatusColor = (status: Teacher['status']) => {
switch (status) {
case 'active':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'inactive':
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: Teacher['status']) => {
switch (status) {
case 'active':
return 'Ativo';
case 'pending':
return 'Pendente';
case 'inactive':
return 'Inativo';
}
};
const filteredTeachers = teachers.filter(t =>
t.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
t.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
(t.subject && t.subject.toLowerCase().includes(searchTerm.toLowerCase()))
);
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Professores</h1>
<button
onClick={handleInviteTeacher}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Convidar Professor
</button>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
</div>
)}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar professores..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">Carregando...</div>
) : filteredTeachers.length === 0 ? (
<div className="p-8 text-center text-gray-500">
Nenhum professor encontrado
</div>
) : (
<div className="divide-y divide-gray-200">
{filteredTeachers.map((teacher) => (
<div
key={teacher.id}
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => handleTeacherClick(teacher.id)}
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium text-gray-900">
{teacher.name}
</h3>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Mail className="h-4 w-4" />
{teacher.email}
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-sm text-gray-500">
<span className="font-medium text-gray-900">
{teacher.class_count}
</span>{' '}
turmas
</div>
<div className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(teacher.status)}`}>
{getStatusText(teacher.status)}
</div>
<button className="p-2 hover:bg-gray-100 rounded-full">
<MoreVertical className="h-5 w-5 text-gray-400" />
</button>
</div>
</div>
{teacher.subject && (
<p className="mt-1 text-sm text-gray-500">
{teacher.subject}
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

114
src/routes.tsx Normal file
View File

@ -0,0 +1,114 @@
import { createBrowserRouter } from 'react-router-dom';
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';
import { ClassesPage } from './pages/dashboard/classes/ClassesPage';
import { CreateClassPage } from './pages/dashboard/classes/CreateClassPage';
import { TeachersPage } from './pages/dashboard/teachers/TeachersPage';
import { InviteTeacherPage } from './pages/dashboard/teachers/InviteTeacherPage';
import { StudentsPage } from './pages/dashboard/students/StudentsPage';
import { AddStudentPage } from './pages/dashboard/students/AddStudentPage';
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '/login',
children: [
{
path: 'school',
element: <LoginForm userType="school" />,
},
{
path: 'teacher',
element: <LoginForm userType="teacher" />,
},
{
path: 'student',
element: <LoginForm userType="student" />,
}
]
},
{
path: '/register',
children: [
{
path: 'school',
element: <SchoolRegistrationForm />,
},
{
path: 'teacher',
element: <RegistrationForm
userType="teacher"
onComplete={(userData) => {
console.log('Registro completo:', userData);
}}
/>,
}
]
},
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{
index: true,
element: <DashboardHome />,
},
{
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: '/demo',
element: <StoryViewer demo={true} />,
},
{
path: '/auth/callback',
element: <AuthCallback />
},
]);

116
src/routes/index.ts Normal file
View File

@ -0,0 +1,116 @@
import { createBrowserRouter } from 'react-router-dom';
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';
import { ClassesPage } from '../pages/dashboard/classes/ClassesPage';
import { CreateClassPage } from '../pages/dashboard/classes/CreateClassPage';
import { TeachersPage } from '../pages/dashboard/teachers/TeachersPage';
import { InviteTeacherPage } from '../pages/dashboard/teachers/InviteTeacherPage';
import { StudentsPage } from '../pages/dashboard/students/StudentsPage';
import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage';
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '/login',
children: [
{
path: 'school',
element: <LoginForm userType="school" />,
},
{
path: 'teacher',
element: <LoginForm userType="teacher" />,
},
{
path: 'student',
element: <LoginForm userType="student" />,
}
]
},
{
path: '/register',
children: [
{
path: 'school',
element: <SchoolRegistrationForm />,
},
{
path: 'teacher',
element: <RegistrationForm
userType="teacher"
onComplete={(userData) => {
// Adicione aqui a lógica para lidar com os dados do usuário
// Por exemplo, redirecionar para outra página ou salvar os dados
console.log(userData);
}}
/>,
}
]
},
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{
index: true,
element: <DashboardHome />,
},
{
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: '/demo',
element: <StoryViewer demo={true} />,
},
{
path: '/auth/callback',
element: <AuthCallback />
},
]);

116
src/routes/index.tsx Normal file
View File

@ -0,0 +1,116 @@
import { createBrowserRouter } from 'react-router-dom';
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';
import { ClassesPage } from '../pages/dashboard/classes/ClassesPage';
import { CreateClassPage } from '../pages/dashboard/classes/CreateClassPage';
import { TeachersPage } from '../pages/dashboard/teachers/TeachersPage';
import { InviteTeacherPage } from '../pages/dashboard/teachers/InviteTeacherPage';
import { StudentsPage } from '../pages/dashboard/students/StudentsPage';
import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage';
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '/login',
children: [
{
path: 'school',
element: <LoginForm userType="school" />,
},
{
path: 'teacher',
element: <LoginForm userType="teacher" />,
},
{
path: 'student',
element: <LoginForm userType="student" />,
}
]
},
{
path: '/register',
children: [
{
path: 'school',
element: <SchoolRegistrationForm />,
},
{
path: 'teacher',
element: <RegistrationForm
userType="teacher"
onComplete={(userData) => {
// Adicione aqui a lógica para lidar com os dados do usuário
// Por exemplo, redirecionar para outra página ou salvar os dados
console.log(userData);
}}
/>,
}
]
},
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{
index: true,
element: <DashboardHome />,
},
{
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: '/demo',
element: <StoryViewer demo={true} />,
},
{
path: '/auth/callback',
element: <AuthCallback />
},
]);

84
src/services/email.ts Normal file
View File

@ -0,0 +1,84 @@
import { Resend } from 'resend';
const resend = new Resend(import.meta.env.VITE_RESEND_API_KEY);
interface SendStudentCredentialsEmailProps {
studentName: string;
studentEmail: string;
password: string;
guardianName: string;
guardianEmail: string;
}
export async function sendStudentCredentialsEmail({
studentName,
studentEmail,
password,
guardianName,
guardianEmail
}: SendStudentCredentialsEmailProps) {
try {
// Email para o aluno
await resend.emails.send({
from: 'Histórias Mágicas <noreply@historias-magicas.com.br>',
to: studentEmail,
subject: 'Bem-vindo ao Histórias Mágicas!',
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #7C3AED;">Olá ${studentName}!</h1>
<p>Bem-vindo ao Histórias Mágicas! Sua conta foi criada com sucesso.</p>
<p>Use as credenciais abaixo para acessar sua conta:</p>
<div style="background-color: #F3F4F6; padding: 16px; border-radius: 8px; margin: 16px 0;">
<p style="margin: 0;"><strong>Email:</strong> ${studentEmail}</p>
<p style="margin: 8px 0 0;"><strong>Senha:</strong> ${password}</p>
</div>
<p style="color: #EF4444; font-size: 14px;">
Por favor, altere sua senha no primeiro acesso.
</p>
<a
href="${import.meta.env.VITE_APP_URL}/login/student"
style="display: inline-block; background-color: #7C3AED; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; margin-top: 16px;"
>
Acessar Plataforma
</a>
</div>
`
});
// Email para o responsável
await resend.emails.send({
from: 'Histórias Mágicas <noreply@historias-magicas.com.br>',
to: guardianEmail,
subject: `Conta do ${studentName} criada no Histórias Mágicas`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #7C3AED;">Olá ${guardianName}!</h1>
<p>
Uma conta foi criada para ${studentName} na plataforma Histórias Mágicas.
Como responsável, você receberá atualizações sobre o progresso e atividades.
</p>
<p>As credenciais de acesso foram enviadas para o email do aluno:</p>
<div style="background-color: #F3F4F6; padding: 16px; border-radius: 8px; margin: 16px 0;">
<p style="margin: 0;"><strong>Email do aluno:</strong> ${studentEmail}</p>
</div>
<p>
Por favor, ajude o aluno a fazer o primeiro acesso e alterar a senha.
Em caso de dúvidas, entre em contato com a escola.
</p>
<div style="font-size: 14px; color: #6B7280; margin-top: 24px;">
<p>
O Histórias Mágicas é uma plataforma educacional que permite aos alunos
criarem e compartilharem histórias interativas, incentivando a criatividade
e o aprendizado.
</p>
</div>
</div>
`
});
return true;
} catch (error) {
console.error('Erro ao enviar emails:', error);
return false;
}
}