From 00d64b136cbec0fc3c3d7ed50919357c045f38cf Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Thu, 19 Dec 2024 19:36:07 -0300 Subject: [PATCH] Correcoes --- .env.example | 4 + .env.production | 4 + .eslintrc.json | 23 ++ netlify.toml | 23 ++ public/book.svg | 4 + .../auth/SchoolRegistrationForm.tsx | 263 ++++++++++++++++ src/components/home/HomePage.tsx | 275 +++++++++++++++++ src/pages/AuthCallback.tsx | 28 ++ src/pages/dashboard/DashboardHome.tsx | 89 ++++++ src/pages/dashboard/DashboardLayout.tsx | 135 +++++++++ src/pages/dashboard/classes/ClassesPage.tsx | 119 ++++++++ .../dashboard/classes/CreateClassPage.tsx | 151 ++++++++++ .../dashboard/students/AddStudentPage.tsx | 283 ++++++++++++++++++ src/pages/dashboard/students/StudentsPage.tsx | 173 +++++++++++ .../dashboard/teachers/InviteTeacherPage.tsx | 148 +++++++++ src/pages/dashboard/teachers/TeachersPage.tsx | 191 ++++++++++++ src/routes.tsx | 114 +++++++ src/routes/index.ts | 116 +++++++ src/routes/index.tsx | 116 +++++++ src/services/email.ts | 84 ++++++ 20 files changed, 2343 insertions(+) create mode 100644 .env.example create mode 100644 .env.production create mode 100644 .eslintrc.json create mode 100644 netlify.toml create mode 100644 public/book.svg create mode 100644 src/components/auth/SchoolRegistrationForm.tsx create mode 100644 src/components/home/HomePage.tsx create mode 100644 src/pages/AuthCallback.tsx create mode 100644 src/pages/dashboard/DashboardHome.tsx create mode 100644 src/pages/dashboard/DashboardLayout.tsx create mode 100644 src/pages/dashboard/classes/ClassesPage.tsx create mode 100644 src/pages/dashboard/classes/CreateClassPage.tsx create mode 100644 src/pages/dashboard/students/AddStudentPage.tsx create mode 100644 src/pages/dashboard/students/StudentsPage.tsx create mode 100644 src/pages/dashboard/teachers/InviteTeacherPage.tsx create mode 100644 src/pages/dashboard/teachers/TeachersPage.tsx create mode 100644 src/routes.tsx create mode 100644 src/routes/index.ts create mode 100644 src/routes/index.tsx create mode 100644 src/services/email.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..588d37b --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +VITE_SUPABASE_URL=your_supabase_url +VITE_SUPABASE_ANON_KEY=your_supabase_anon_key +VITE_RESEND_API_KEY=your_resend_api_key +VITE_APP_URL=http://localhost:5173 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..d40078d --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +VITE_SUPABASE_URL=https://bsjlbnyslxzsdwxvkaap.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzamxibnlzbHh6c2R3eHZrYWFwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ2MzUzNzYsImV4cCI6MjA1MDIxMTM3Nn0.ygEUrAu2ZnCkfgS4-k4Puvk7ywkn3U7Bnzh7BSOQWFo +VITE_RESEND_API_KEY=GEoM_cVt4qyBFVkngJWi8wBrMWOiPMUAuxuFGykcP0A +VITE_APP_URL=https://historiasmagicas.netlify.app/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..efcefb1 --- /dev/null +++ b/.eslintrc.json @@ -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" + } +} \ No newline at end of file diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..cb5419c --- /dev/null +++ b/netlify.toml @@ -0,0 +1,23 @@ +[build] + command = "npm run build" + publish = "dist" + +[build.environment] + NODE_VERSION = "18" + VITE_SUPABASE_URL = "https://bsjlbnyslxzsdwxvkaap.supabase.co" + VITE_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzamxibnlzbHh6c2R3eHZrYWFwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ2MzUzNzYsImV4cCI6MjA1MDIxMTM3Nn0.ygEUrAu2ZnCkfgS4-k4Puvk7ywkn3U7Bnzh7BSOQWFo" + VITE_RESEND_API_KEY = "GEoM_cVt4qyBFVkngJWi8wBrMWOiPMUAuxuFGykcP0A" + VITE_APP_URL = "https://historiasmagicas.netlify.app/" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +[dev] + command = "npm run dev" + port = 5173 + publish = "dist" + +[functions] + node_bundler = "esbuild" \ No newline at end of file diff --git a/public/book.svg b/public/book.svg new file mode 100644 index 0000000..eb07585 --- /dev/null +++ b/public/book.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/auth/SchoolRegistrationForm.tsx b/src/components/auth/SchoolRegistrationForm.tsx new file mode 100644 index 0000000..e60aeb0 --- /dev/null +++ b/src/components/auth/SchoolRegistrationForm.tsx @@ -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) => { + 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 ( +
+
+
+
+ +
+

+ Cadastro de Escola +

+

+ Comece a transformar a educação com histórias interativas +

+
+ + {(error || authError) && ( +
+ {error || authError} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ + +
+ +
+

+ Já tem uma conta?{' '} + +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx new file mode 100644 index 0000000..b3f6475 --- /dev/null +++ b/src/components/home/HomePage.tsx @@ -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 ( +
+ {/* Header/Nav */} + + + {/* Hero Section */} +
+
+
+

+ Transforme a Educação com + Histórias Interativas +

+

+ Uma plataforma educacional que conecta escolas, professores e alunos através + de histórias personalizadas e experiências de aprendizado únicas. +

+
+ + +
+
+
+
+ + {/* Features Grid */} +
+
+
+

+ Tudo que você precisa em um só lugar +

+

+ Uma plataforma completa para criar, gerenciar e compartilhar histórias educativas +

+
+ +
+ } + title="Gestão Escolar" + description="Gerencie turmas, professores e alunos de forma simples e eficiente" + /> + } + title="Histórias Interativas" + description="Crie e compartilhe histórias educativas personalizadas" + /> + } + title="Colaboração" + description="Trabalhe em conjunto com outros professores e compartilhe recursos" + /> + } + title="Ambiente Seguro" + description="Proteção de dados e conteúdo adequado para crianças" + /> + } + title="Personalização" + description="Adapte o conteúdo ao perfil e necessidades de cada aluno" + /> + } + title="Acompanhamento" + description="Monitore o progresso e engajamento dos alunos" + /> +
+
+
+ + {/* Stats Section */} +
+
+
+ + + +
+
+
+ + {/* CTA Section */} +
+
+
+

+ Comece sua jornada hoje +

+

+ Transforme a educação em sua escola com histórias interativas +

+ +
+
+
+ + {/* Footer */} + +
+ ); +} + +interface FeatureCardProps { + icon: React.ReactNode; + title: string; + description: string; +} + +function FeatureCard({ icon, title, description }: FeatureCardProps) { + return ( +
+
+ {icon} +
+

{title}

+

{description}

+
+ ); +} + +interface StatCardProps { + number: string; + label: string; +} + +function StatCard({ number, label }: StatCardProps) { + return ( +
+
{number}
+
{label}
+
+ ); +} \ No newline at end of file diff --git a/src/pages/AuthCallback.tsx b/src/pages/AuthCallback.tsx new file mode 100644 index 0000000..ae79aed --- /dev/null +++ b/src/pages/AuthCallback.tsx @@ -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 ( +
+
+

+ Verificando autenticação... +

+

+ Por favor, aguarde enquanto confirmamos seu acesso. +

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/DashboardHome.tsx b/src/pages/dashboard/DashboardHome.tsx new file mode 100644 index 0000000..1115fdf --- /dev/null +++ b/src/pages/dashboard/DashboardHome.tsx @@ -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 ( +
+

Dashboard

+ + {error && ( +
+ {error} +
+ )} + +
+
+
+
+ +
+
+

Total de Turmas

+

+ {loading ? '...' : stats.totalClasses} +

+
+
+
+ +
+
+
+ +
+
+

Total de Professores

+

+ {loading ? '...' : stats.totalTeachers} +

+
+
+
+ +
+
+
+ +
+
+

Total de Alunos

+

+ {loading ? '...' : stats.totalStudents} +

+
+
+
+
+ +
+
+

+ Últimas Turmas +

+ {/* Lista de últimas turmas */} +
+ +
+

+ Histórias Recentes +

+ {/* Lista de histórias recentes */} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/DashboardLayout.tsx b/src/pages/dashboard/DashboardLayout.tsx new file mode 100644 index 0000000..850a589 --- /dev/null +++ b/src/pages/dashboard/DashboardLayout.tsx @@ -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 ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/classes/ClassesPage.tsx b/src/pages/dashboard/classes/ClassesPage.tsx new file mode 100644 index 0000000..30af112 --- /dev/null +++ b/src/pages/dashboard/classes/ClassesPage.tsx @@ -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([]); + 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 ( +
+
+

Turmas

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + 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" + /> +
+
+ + {loading ? ( +
Carregando...
+ ) : filteredClasses.length === 0 ? ( +
+ Nenhuma turma encontrada +
+ ) : ( +
+ {filteredClasses.map((classItem) => ( +
handleClassClick(classItem.id)} + > +
+
+

+ {classItem.name} +

+

+ {classItem.grade} - {classItem.year} +

+
+
+
+ + {classItem.teacher_count} + {' '} + professores +
+
+ + {classItem.student_count} + {' '} + alunos +
+ +
+
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/classes/CreateClassPage.tsx b/src/pages/dashboard/classes/CreateClassPage.tsx new file mode 100644 index 0000000..4188304 --- /dev/null +++ b/src/pages/dashboard/classes/CreateClassPage.tsx @@ -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(null); + const [formData, setFormData] = useState>({ + 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 ( +
+ + +
+

Nova Turma

+ + {formError && ( +
+ {formError} +
+ )} + +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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} + /> +
+ +
+ + +
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/students/AddStudentPage.tsx b/src/pages/dashboard/students/AddStudentPage.tsx new file mode 100644 index 0000000..f5a98d9 --- /dev/null +++ b/src/pages/dashboard/students/AddStudentPage.tsx @@ -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([]); + const [formData, setFormData] = React.useState({ + name: '', + email: '', + class_id: '', + guardian_name: '', + guardian_email: '', + guardian_phone: '', + }); + const [formError, setFormError] = React.useState(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 ( +
+ + +
+

Adicionar Aluno

+ + {formError && ( +
+ {formError} +
+ )} + +
+
+ + 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 + /> +
+ +
+ + 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 + /> +
+ +
+ + +
+ +
+

+ Informações do Responsável +

+ +
+
+ + 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 + /> +
+ +
+ + 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 + /> +
+ +
+ + 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 + /> +
+
+
+ +
+ +
+
+
+
+ ); +} + +// 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; +} \ No newline at end of file diff --git a/src/pages/dashboard/students/StudentsPage.tsx b/src/pages/dashboard/students/StudentsPage.tsx new file mode 100644 index 0000000..cb67e71 --- /dev/null +++ b/src/pages/dashboard/students/StudentsPage.tsx @@ -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([]); + 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 ( +
+
+

Alunos

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + 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" + /> +
+
+ + {loading ? ( +
Carregando...
+ ) : filteredStudents.length === 0 ? ( +
+ Nenhum aluno encontrado +
+ ) : ( +
+ {filteredStudents.map((student) => ( +
handleStudentClick(student.id)} + > +
+
+

+ {student.name} +

+
+ + {student.class_name} +
+
+
+
+ + {student.stories_count} + {' '} + histórias +
+
+ {getStatusText(student.status)} +
+ +
+
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dashboard/teachers/InviteTeacherPage.tsx b/src/pages/dashboard/teachers/InviteTeacherPage.tsx new file mode 100644 index 0000000..972a9c6 --- /dev/null +++ b/src/pages/dashboard/teachers/InviteTeacherPage.tsx @@ -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(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 ( +
+ + +
+

Convidar Professor

+ + {formError && ( +
+ {formError} +
+ )} + +
+
+ + 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 + /> +
+ +
+ + 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 + /> +
+ +
+ + 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" + /> +
+ +
+ +