This commit is contained in:
2026-01-22 10:40:39 +00:00
parent 632aed8181
commit cd29acdd23
9 changed files with 645 additions and 17 deletions

View File

@@ -9,7 +9,7 @@ import {
Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
} from 'react-native';
import { Calendar, LocaleConfig } from 'react-native-calendars';
import { useTheme } from '../themecontext';
import { useTheme } from '../../themecontext';
// Configuração PT
LocaleConfig.locales['pt'] = {

View File

@@ -1,19 +1,19 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React, { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
import { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
import {
Alert,
Linking,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
Alert,
Linking,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
} from 'react-native';
import { useTheme } from '../themecontext';
import { useTheme } from '../../themecontext';
const Definicoes = memo(() => {
const router = useRouter();

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useTheme } from '../themecontext'; // Ajusta o caminho conforme a tua estrutura
import { useTheme } from '../../themecontext'; // Ajusta o caminho conforme a tua estrutura
const MainMenu = () => {
const { isDarkMode } = useTheme();

View File

@@ -8,7 +8,7 @@ import {
Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet,
Text, TouchableOpacity, View
} from 'react-native';
import { useTheme } from '../themecontext';
import { useTheme } from '../../themecontext';
export default function Perfil() {
const { isDarkMode } = useTheme();

View File

@@ -0,0 +1,97 @@
import {
StyleSheet
} from 'react-native';
/* DADOS MOCK (SUBSTITUI PELO SUPABASE DEPOIS) */
const alunos = [
{
id: '1',
nome: 'Ana Martins',
curso: 'Técnico de Informática',
turma: '12ºTIG',
empresa: 'Tech Solutions',
},
{
id: '2',
nome: 'Pedro Costa',
curso: 'Técnico de Informática',
turma: '11ºTIG',
empresa: 'SoftDev Lda',
},
{
id: '3',
nome: 'Rita Fernandes',
curso: 'Técnico de Informática',
turma: '12ºTIG',
empresa: 'WebWorks',
},
];
/* ESTILOS */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f1f3f5',
},
content: {
padding: 20,
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
title: {
flex: 1,
textAlign: 'center',
fontSize: 20,
fontWeight: '800',
color: '#212529',
},
card: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 3,
},
avatar: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#0d6efd',
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
},
avatarText: {
color: '#fff',
fontWeight: '800',
fontSize: 18,
},
info: {
flex: 1,
},
nome: {
fontSize: 16,
fontWeight: '700',
color: '#212529',
},
sub: {
fontSize: 13,
color: '#6c757d',
marginTop: 2,
},
empresa: {
fontSize: 13,
color: '#495057',
marginTop: 4,
},
});

View File

@@ -0,0 +1,254 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
Alert,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { supabase } from '../lib/supabase';
export default function PerfilProfessor() {
const router = useRouter();
const [editando, setEditando] = useState(false);
const [perfil, setPerfil] = useState({
nome: 'João Miranda',
email: 'joao.miranda@epvc.pt',
mecanografico: 'PROF-174',
area: 'Informática',
turmas: '12ºINF | 11ºINF | 10ºINF',
funcao: 'Orientador de Estágio',
});
const terminarSessao = async () => {
await supabase.auth.signOut();
router.replace('/');
};
const guardarPerfil = () => {
setEditando(false);
Alert.alert('Sucesso', 'Perfil atualizado com sucesso!');
// aqui depois ligas ao Supabase (update)
};
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
{/* TOPO COM VOLTAR */}
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back-outline" size={26} color="#212529" />
</TouchableOpacity>
<Text style={styles.topTitle}>Perfil</Text>
<View style={{ width: 26 }} />
</View>
{/* HEADER */}
<View style={styles.header}>
<View style={styles.avatar}>
<Ionicons name="person" size={48} color="#fff" />
</View>
<Text style={styles.name}>{perfil.nome}</Text>
<Text style={styles.role}>{perfil.funcao}</Text>
</View>
{/* INFORMAÇÕES */}
<View style={styles.card}>
<InfoField label="Nome" value={perfil.nome} editable={editando}
onChange={(v) => setPerfil({ ...perfil, nome: v })}
/>
<InfoField label="Email" value={perfil.email} editable={false} />
<InfoField label="Nº Mecanográfico" value={perfil.mecanografico} editable={editando}
onChange={(v) => setPerfil({ ...perfil, mecanografico: v })}
/>
<InfoField label="Área" value={perfil.area} editable={editando}
onChange={(v) => setPerfil({ ...perfil, area: v })}
/>
<InfoField label="Turmas" value={perfil.turmas} editable={editando}
onChange={(v) => setPerfil({ ...perfil, turmas: v })}
/>
</View>
{/* AÇÕES */}
<View style={styles.actions}>
{editando ? (
<TouchableOpacity style={styles.primaryButton} onPress={guardarPerfil}>
<Ionicons name="save-outline" size={20} color="#fff" />
<Text style={styles.primaryText}>Guardar alterações</Text>
</TouchableOpacity>
) : (
<ActionButton
icon="create-outline"
text="Editar perfil"
onPress={() => setEditando(true)}
/>
)}
<ActionButton
icon="key-outline"
text="Alterar palavra-passe"
onPress={() => router.push('/redefenirsenha2')}
/>
<ActionButton
icon="log-out-outline"
text="Terminar sessão"
danger
onPress={terminarSessao}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
/* COMPONENTES */
function InfoField({
label,
value,
editable,
onChange,
}: {
label: string;
value: string;
editable: boolean;
onChange?: (v: string) => void;
}) {
return (
<View style={styles.infoField}>
<Text style={styles.label}>{label}</Text>
{editable ? (
<TextInput
value={value}
onChangeText={onChange}
style={styles.input}
/>
) : (
<Text style={styles.value}>{value}</Text>
)}
</View>
);
}
function ActionButton({
icon,
text,
onPress,
danger = false,
}: {
icon: any;
text: string;
onPress: () => void;
danger?: boolean;
}) {
return (
<TouchableOpacity
style={[styles.actionButton, danger && styles.dangerButton]}
onPress={onPress}
>
<Ionicons name={icon} size={20} color={danger ? '#dc3545' : '#0d6efd'} />
<Text style={[styles.actionText, danger && styles.dangerText]}>
{text}
</Text>
</TouchableOpacity>
);
}
/* ESTILOS */
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f1f3f5' },
content: { padding: 24 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 20,
},
topTitle: {
fontSize: 18,
fontWeight: '700',
color: '#212529',
},
header: { alignItems: 'center', marginBottom: 32 },
avatar: {
width: 90, height: 90, borderRadius: 45,
backgroundColor: '#0d6efd',
alignItems: 'center', justifyContent: 'center',
marginBottom: 12,
},
name: { fontSize: 22, fontWeight: '800', color: '#212529' },
role: { fontSize: 14, color: '#6c757d', marginTop: 4 },
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 20,
marginBottom: 24,
elevation: 4,
},
infoField: { marginBottom: 16 },
label: { fontSize: 12, color: '#6c757d', marginBottom: 4 },
value: { fontSize: 15, fontWeight: '600', color: '#212529' },
input: {
borderWidth: 1,
borderColor: '#ced4da',
borderRadius: 10,
padding: 10,
fontSize: 15,
backgroundColor: '#f8f9fa',
},
actions: { gap: 12 },
actionButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: 14,
padding: 16,
},
actionText: {
fontSize: 15,
fontWeight: '600',
marginLeft: 12,
color: '#0d6efd',
},
primaryButton: {
backgroundColor: '#0d6efd',
borderRadius: 14,
padding: 16,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
primaryText: { color: '#fff', fontWeight: '700', marginLeft: 8 },
dangerButton: {
borderWidth: 1,
borderColor: '#dc3545',
},
dangerText: { color: '#dc3545' },
});

View File

@@ -0,0 +1,147 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
export default function ProfessorMenu() {
const router = useRouter();
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
{/* HEADER */}
<View style={styles.header}>
<Text style={styles.welcome}>Bem-vindo,</Text>
<Text style={styles.name}>Professor 👨🏫</Text>
<Text style={styles.subtitle}>Painel de gestão</Text>
</View>
{/* GRID DE OPÇÕES */}
<View style={styles.grid}>
<MenuCard
icon="person-outline"
title="Perfil"
subtitle="Dados pessoais"
onPress={() => router.push('/perfil')}
/>
<MenuCard
icon="settings-outline"
title="Definições"
subtitle="Configurações"
onPress={() => router.push('/definicoes')}
/>
<MenuCard
icon="document-text-outline"
title="Sumários"
subtitle="Registos diários"
onPress={() => router.push('/professor/sumarios')}
/>
<MenuCard
icon="people-outline"
title="Alunos"
subtitle="Gerir alunos"
onPress={() => router.push('/professor/alunos')}
/>
<MenuCard
icon="business-outline"
title="Empresas"
subtitle="Entidades de estágio"
onPress={() => router.push('/professor/empresas')}
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
/* CARD REUTILIZÁVEL */
function MenuCard({
icon,
title,
subtitle,
onPress,
}: {
icon: any;
title: string;
subtitle: string;
onPress: () => void;
}) {
return (
<TouchableOpacity style={styles.card} onPress={onPress}>
<Ionicons name={icon} size={28} color="#0d6efd" />
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardSubtitle}>{subtitle}</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f1f3f5',
},
content: {
padding: 24,
},
header: {
marginBottom: 32,
},
welcome: {
fontSize: 16,
color: '#6c757d',
},
name: {
fontSize: 28,
fontWeight: '800',
color: '#212529',
marginTop: 4,
},
subtitle: {
fontSize: 14,
color: '#6c757d',
marginTop: 6,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
card: {
width: '48%',
backgroundColor: '#fff',
borderRadius: 18,
padding: 20,
marginBottom: 16,
alignItems: 'flex-start',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 10,
elevation: 4,
},
cardTitle: {
fontSize: 16,
fontWeight: '700',
marginTop: 12,
color: '#212529',
},
cardSubtitle: {
fontSize: 13,
color: '#6c757d',
marginTop: 4,
},
});

View File

@@ -0,0 +1,131 @@
// app/forgot-password.tsx
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../lib/supabase';
export default function ForgotPassword() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSendResetEmail = async () => {
if (!email) {
Alert.alert('Atenção', 'Insira seu email');
return;
}
setLoading(true);
try {
const { error } = await supabase.auth.resetPasswordForEmail(email);
if (error) throw error;
Alert.alert('Sucesso!', 'Verifique seu email para redefinir a palavra-passe');
router.back(); // volta para login
} catch (err: any) {
Alert.alert('Erro', err.message);
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: '#f8f9fa' }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<View style={styles.container}>
<Text style={styles.title}>Recuperar Palavra-passe</Text>
<Text style={styles.subtitle}>Insira seu email para receber o link de redefinição</Text>
{/* INPUT EMAIL */}
<TextInput
style={styles.input}
placeholder="email@address.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
{/* BOTÃO ENVIAR LINK */}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendResetEmail}
disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>ENVIAR LINK</Text>}
</TouchableOpacity>
{/* BOTÃO VOLTAR */}
<TouchableOpacity onPress={() => router.push('/Professor/PerfilProf')} style={styles.backContainer}>
<Text style={styles.backText}> Voltar atrás</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
scrollContainer: { flexGrow: 1, justifyContent: 'center', padding: 24 },
container: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
logo: {
width: 120,
height: 120,
alignSelf: 'center',
marginBottom: 20,
},
title: { fontSize: 24, fontWeight: '700', color: '#2d3436', marginBottom: 8, textAlign: 'center' },
subtitle: { fontSize: 14, color: '#636e72', marginBottom: 20, textAlign: 'center' },
input: {
backgroundColor: '#f1f2f6',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
marginBottom: 20,
borderWidth: 0,
color: '#2d3436',
},
button: {
backgroundColor: '#0984e3',
borderRadius: 12,
paddingVertical: 16,
alignItems: 'center',
marginBottom: 12,
shadowColor: '#0984e3',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 3,
},
buttonDisabled: { backgroundColor: '#74b9ff' },
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
backContainer: { marginTop: 8, alignItems: 'center' },
backText: { color: '#0984e3', fontSize: 15, fontWeight: '500' },
});

View File

@@ -7,7 +7,7 @@ export default function LoginScreen() {
const router = useRouter();
const handleLoginSuccess = () => {
router.replace('/AlunoHome');
router.replace('/Professor/ProfessorHome');
};
return (