mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
Correcoes
This commit is contained in:
parent
26888e9824
commit
677ee422c4
23
.eslintrc.json
Normal file
23
.eslintrc.json
Normal 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
4
public/book.svg
Normal 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 |
263
src/components/auth/SchoolRegistrationForm.tsx
Normal file
263
src/components/auth/SchoolRegistrationForm.tsx
Normal 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">
|
||||
Já 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>
|
||||
);
|
||||
}
|
||||
275
src/components/home/HomePage.tsx
Normal file
275
src/components/home/HomePage.tsx
Normal 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 só 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">
|
||||
© 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>
|
||||
);
|
||||
}
|
||||
28
src/pages/AuthCallback.tsx
Normal file
28
src/pages/AuthCallback.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
89
src/pages/dashboard/DashboardHome.tsx
Normal file
89
src/pages/dashboard/DashboardHome.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
135
src/pages/dashboard/DashboardLayout.tsx
Normal file
135
src/pages/dashboard/DashboardLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
119
src/pages/dashboard/classes/ClassesPage.tsx
Normal file
119
src/pages/dashboard/classes/ClassesPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
151
src/pages/dashboard/classes/CreateClassPage.tsx
Normal file
151
src/pages/dashboard/classes/CreateClassPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
283
src/pages/dashboard/students/AddStudentPage.tsx
Normal file
283
src/pages/dashboard/students/AddStudentPage.tsx
Normal 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;
|
||||
}
|
||||
173
src/pages/dashboard/students/StudentsPage.tsx
Normal file
173
src/pages/dashboard/students/StudentsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
src/pages/dashboard/teachers/InviteTeacherPage.tsx
Normal file
148
src/pages/dashboard/teachers/InviteTeacherPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
191
src/pages/dashboard/teachers/TeachersPage.tsx
Normal file
191
src/pages/dashboard/teachers/TeachersPage.tsx
Normal 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
114
src/routes.tsx
Normal 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
116
src/routes/index.ts
Normal 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
116
src/routes/index.tsx
Normal 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
84
src/services/email.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user