mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27: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