dados perfil aluno
This commit is contained in:
@@ -22,8 +22,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { Calendar, LocaleConfig } from 'react-native-calendars';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
// Configuração PT-PT para o Calendário
|
||||
LocaleConfig.locales['pt'] = {
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const Definicoes = memo(() => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// app/Aluno/PerfilAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
@@ -16,60 +16,102 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
export default function PerfilAluno() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// --- ESTADOS ---
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [perfil, setPerfil] = useState<any>(null);
|
||||
const [estagio, setEstagio] = useState<any>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// --- ANIMAÇÕES ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
const fadeAnim = useMemo(() => new Animated.Value(0), []);
|
||||
const [faltasJustificadas, setFaltasJustificadas] = useState(0);
|
||||
const [faltasInjustificadas, setFaltasInjustificadas] = useState(0);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
const fadeAnim = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azul: '#2390a6',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
|
||||
vermelho: '#EF4444',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.05)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
setAlertConfig({ msg, type });
|
||||
Animated.sequence([
|
||||
Animated.timing(alertOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
|
||||
Animated.delay(3000),
|
||||
Animated.timing(alertOpacity, { toValue: 0, duration: 400, useNativeDriver: true })
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
// --- LÓGICA DE VERIFICAÇÃO DO ESTÁGIO ATIVO ---
|
||||
const isEstagioAtivo = useMemo(() => {
|
||||
if (!estagio) return false; // Se não tem estágio na DB, não está ativo
|
||||
if (!estagio.data_fim) return true; // Se tem estágio mas sem data de fim, assumimos ativo
|
||||
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0); // Reset às horas para comparar só o dia
|
||||
|
||||
const dataFim = new Date(estagio.data_fim);
|
||||
dataFim.setHours(0, 0, 0, 0);
|
||||
|
||||
return dataFim >= hoje; // Se a data de fim for hoje ou no futuro, está ativo
|
||||
}, [estagio]);
|
||||
|
||||
// --- FUNÇÕES DE DATA ---
|
||||
const formatarDataParaUI = (dataDB: string) => {
|
||||
if (!dataDB) return '';
|
||||
const parts = dataDB.split('-');
|
||||
if (parts.length !== 3) return dataDB;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const formatarDataParaDB = (dataUI: string) => {
|
||||
if (!dataUI || dataUI.length !== 10) return null;
|
||||
const parts = dataUI.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataPT: string) => {
|
||||
if (!dataPT || dataPT.length !== 10) return null;
|
||||
const parts = dataPT.split('-');
|
||||
const dia = parseInt(parts[0], 10);
|
||||
const mes = parseInt(parts[1], 10);
|
||||
const ano = parseInt(parts[2], 10);
|
||||
|
||||
if (ano < 1950 || ano > new Date().getFullYear()) return null;
|
||||
|
||||
const hoje = new Date();
|
||||
const nascimento = new Date(ano, mes - 1, dia);
|
||||
|
||||
if (nascimento.getDate() !== dia) return null;
|
||||
|
||||
let idade = hoje.getFullYear() - nascimento.getFullYear();
|
||||
const m = hoje.getMonth() - nascimento.getMonth();
|
||||
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) {
|
||||
idade--;
|
||||
}
|
||||
return idade >= 0 ? idade : 0;
|
||||
};
|
||||
|
||||
const aplicarMascaraData = (text: string) => {
|
||||
const cleaned = text.replace(/\D/g, '');
|
||||
let formatted = cleaned;
|
||||
if (cleaned.length > 2) formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
if (cleaned.length > 4) formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
if (cleaned.length > 2 && cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
} else if (cleaned.length > 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// --- CARREGAMENTO DE DADOS COM JOIN DE HORÁRIOS ---
|
||||
// --- FIM FUNÇÕES DATA ---
|
||||
|
||||
const buscarDados = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -84,23 +126,35 @@ export default function PerfilAluno() {
|
||||
if (alunoRes) aData = alunoRes;
|
||||
}
|
||||
|
||||
// Join com a tabela horarios_estagio
|
||||
const { data: eData } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
*,
|
||||
empresas(nome, tutor_nome),
|
||||
horarios_estagio(periodo, hora_inicio, hora_fim)
|
||||
`)
|
||||
.select(`*, empresas(*), horarios_estagio(*)`)
|
||||
.eq('aluno_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
setPerfil({ ...pData, ...aData, email: user.email });
|
||||
const { data: presencas } = await supabase.from('presencas').select('estado, justificacao_url').eq('aluno_id', user.id);
|
||||
|
||||
if (presencas) {
|
||||
setFaltasJustificadas(presencas.filter(p => p.estado === 'Falta' && p.justificacao_url).length);
|
||||
setFaltasInjustificadas(presencas.filter(p => p.estado === 'Falta' && !p.justificacao_url).length);
|
||||
}
|
||||
|
||||
const dataFormatadaUI = formatarDataParaUI(pData?.data_nascimento);
|
||||
const idadeCalculada = dataFormatadaUI ? calcularIdade(dataFormatadaUI) : pData?.idade;
|
||||
|
||||
setPerfil({
|
||||
...aData,
|
||||
...pData,
|
||||
email: user.email,
|
||||
data_nascimento: dataFormatadaUI,
|
||||
idade: idadeCalculada ?? 'N/A'
|
||||
});
|
||||
|
||||
setEstagio(eData);
|
||||
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
|
||||
} catch (err) {
|
||||
showAlert('Erro ao sincronizar dados.', 'error');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -111,20 +165,30 @@ export default function PerfilAluno() {
|
||||
const salvarDados = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const { error } = await supabase.from('profiles').update({
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const dataProntaParaDB = formatarDataParaDB(perfil.data_nascimento);
|
||||
|
||||
const { error, data } = await supabase.from('profiles').update({
|
||||
nome: perfil.nome,
|
||||
telefone: perfil.telefone,
|
||||
residencia: perfil.residencia,
|
||||
idade: perfil.idade,
|
||||
data_nascimento: perfil.data_nascimento
|
||||
}).eq('id', perfil.id);
|
||||
data_nascimento: dataProntaParaDB,
|
||||
idade: perfil.idade !== 'N/A' ? Number(perfil.idade) : null,
|
||||
telefone: perfil.telefone
|
||||
}).eq('id', user.id).select();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("Erro de RLS. Confirma as tuas políticas no Supabase.");
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
showAlert('Perfil guardado!', 'success');
|
||||
buscarDados();
|
||||
} catch (e) {
|
||||
showAlert('Falha ao guardar.', 'error');
|
||||
Alert.alert("Sucesso", "Perfil atualizado!");
|
||||
await buscarDados();
|
||||
} catch (e: any) {
|
||||
Alert.alert("Erro ao gravar", e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -135,15 +199,8 @@ export default function PerfilAluno() {
|
||||
return (
|
||||
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: cores.fundo }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{alertConfig && (
|
||||
<Animated.View style={[styles.alert, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? cores.vermelho : cores.verde, top: insets.top + 10 }]}>
|
||||
<Ionicons name="information-circle" size={20} color="#fff" />
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
|
||||
<View style={styles.headerContainer}>
|
||||
<TouchableOpacity style={[styles.roundBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.texto} />
|
||||
@@ -161,86 +218,115 @@ export default function PerfilAluno() {
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
||||
<Animated.View style={{ opacity: fadeAnim }}>
|
||||
|
||||
{/* CABEÇALHO */}
|
||||
<View style={styles.avatarSection}>
|
||||
<View style={[styles.avatarFrame, { borderColor: cores.azulSuave }]}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarInitial}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarInitial}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<Text style={[styles.profileName, { color: cores.texto }]}>{perfil?.nome}</Text>
|
||||
<TextInput
|
||||
style={[styles.profileName, { color: cores.texto, borderBottomWidth: isEditing ? 1 : 0, borderBottomColor: cores.borda }]}
|
||||
value={perfil?.nome}
|
||||
editable={isEditing}
|
||||
onChangeText={(v) => setPerfil({...perfil, nome: v})}
|
||||
placeholder="Nome do Aluno"
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.badgeText, { color: cores.azul }]}>{perfil?.tipo?.toUpperCase()}</Text>
|
||||
<Text style={[styles.badgeText, { color: cores.azul }]}>{perfil?.turma_curso || 'CURSO NÃO DEFINIDO'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DADOS ESCOLARES */}
|
||||
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Registo Escolar</Text>
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}>
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card }]}>
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 1, marginRight: 12 }}><PerfilInput label="Nº Escola" icon="id-card-outline" value={perfil?.n_escola?.toString()} editable={false} cores={cores} /></View>
|
||||
<View style={{ flex: 1 }}><PerfilInput label="Ano" icon="calendar-outline" value={perfil?.ano?.toString() + 'º Ano'} editable={false} cores={cores} /></View>
|
||||
</View>
|
||||
<PerfilInput label="Turma e Curso" icon="school-outline" value={perfil?.turma_curso} editable={false} cores={cores} />
|
||||
</View>
|
||||
|
||||
{/* DADOS PESSOAIS */}
|
||||
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Contactos e Pessoal</Text>
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}>
|
||||
<PerfilInput label="Email" icon="mail-outline" value={perfil?.email} editable={false} cores={cores} />
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 0.8, marginRight: 12 }}><PerfilInput label="Idade" icon="time-outline" value={perfil?.idade?.toString()} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, idade: v})} cores={cores} keyboardType="numeric" /></View>
|
||||
<View style={{ flex: 1.2 }}><PerfilInput label="Telemóvel" icon="call-outline" value={perfil?.telefone} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" maxLength={9} /></View>
|
||||
</View>
|
||||
<PerfilInput label="Nascimento" icon="gift-outline" value={perfil?.data_nascimento} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} cores={cores} placeholder="DD-MM-AAAA" />
|
||||
</View>
|
||||
|
||||
{/* SECÇÃO ESTÁGIO COM LOGICA DE HORARIOS RELACIONADOS */}
|
||||
{estagio && (
|
||||
<>
|
||||
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Informação de Estágio</Text>
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderLeftWidth: 4, borderLeftColor: cores.azul, shadowColor: cores.sombra }]}>
|
||||
<PerfilInput label="Empresa" icon="business-outline" value={estagio.empresas?.nome} editable={false} cores={cores} />
|
||||
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 1, marginRight: 12 }}>
|
||||
<PerfilInput label="Horas" icon="timer-outline" value={estagio.horas_totais?.toString() + 'h'} editable={false} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 2 }}>
|
||||
<PerfilInput
|
||||
label="Horário"
|
||||
icon="watch-outline"
|
||||
value={
|
||||
estagio.horarios_estagio?.length > 0
|
||||
? estagio.horarios_estagio.map((h: any) =>
|
||||
`${h.periodo}: ${h.hora_inicio.slice(0,5)}-${h.hora_fim.slice(0,5)}`
|
||||
).join('\n')
|
||||
: "Não definido"
|
||||
}
|
||||
editable={false}
|
||||
cores={cores}
|
||||
multiline={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{estagio.empresas?.tutor_nome && <PerfilInput label="Tutor" icon="person-circle-outline" value={estagio.empresas.tutor_nome} editable={false} cores={cores} />}
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<PerfilInput label="Nº Escola" icon="id-card-outline" value={perfil?.n_escola?.toString()} editable={false} cores={cores} />
|
||||
</View>
|
||||
</>
|
||||
<View style={{ flex: 1 }}>
|
||||
<PerfilInput label="Ano" icon="calendar-outline" value={perfil?.ano ? `${perfil.ano}º Ano` : 'N/A'} editable={false} cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
<PerfilInput label="Email Institucional" icon="mail-outline" value={perfil?.email} editable={false} cores={cores} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Dados Pessoais</Text>
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card }]}>
|
||||
<PerfilInput
|
||||
label="Data Nascimento" icon="gift-outline"
|
||||
value={perfil?.data_nascimento}
|
||||
editable={isEditing}
|
||||
onChangeText={(v:string) => {
|
||||
const masked = aplicarMascaraData(v);
|
||||
const novaIdade = calcularIdade(masked);
|
||||
setPerfil({...perfil, data_nascimento: masked, idade: novaIdade ?? 'N/A'});
|
||||
}}
|
||||
cores={cores}
|
||||
placeholder="DD-MM-AAAA"
|
||||
maxLength={10}
|
||||
/>
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<PerfilInput label="Idade" icon="time-outline" value={perfil?.idade?.toString()} editable={false} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 2 }}>
|
||||
<PerfilInput label="Telemóvel" icon="call-outline" value={perfil?.telefone} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" />
|
||||
</View>
|
||||
</View>
|
||||
<PerfilInput label="Residência" icon="location-outline" value={perfil?.residencia} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, residencia: v})} cores={cores} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Informação de Estágio</Text>
|
||||
|
||||
{/* Aqui entra a condição: Só mostra a info se isEstagioAtivo for verdadeiro */}
|
||||
{isEstagioAtivo ? (
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderLeftWidth: 4, borderLeftColor: cores.azul }]}>
|
||||
<PerfilInput label="Entidade de Acolhimento" icon="business-outline" value={estagio.empresas?.nome} editable={false} cores={cores} />
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<PerfilInput label="Horas Totais" icon="timer-outline" value={`${estagio.horas_totais}h`} editable={false} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<PerfilInput label="Realizadas" icon="checkmark-done-outline" value={`${estagio.horas_concluidas || 0}h`} editable={false} cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.inputRow, { marginTop: 10 }]}>
|
||||
<View style={[styles.miniStatus, { backgroundColor: cores.azulSuave, flex: 1, marginRight: 10 }]}>
|
||||
<Text style={[styles.miniStatusText, { color: cores.azul }]}>JUSTIFICADAS: {faltasJustificadas}</Text>
|
||||
</View>
|
||||
<View style={[styles.miniStatus, { backgroundColor: cores.vermelhoSuave, flex: 1 }]}>
|
||||
<Text style={[styles.miniStatusText, { color: cores.vermelho }]}>INJUSTIFICADAS: {faltasInjustificadas}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, alignItems: 'center', padding: 30, borderStyle: 'dashed', borderWidth: 1, borderColor: cores.borda }]}>
|
||||
<Ionicons name="warning-outline" size={30} color={cores.secundario} />
|
||||
{/* Mensagem dinâmica se já teve ou se nunca teve estágio */}
|
||||
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700', textAlign: 'center' }}>
|
||||
{estagio ? "Sem estágio ativo no momento." : "Sem estágio atribuído no sistema"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ACÇÕES */}
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity style={[styles.actionMenuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/Aluno/redefenirsenha')}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: cores.azulSuave }]}><Ionicons name="key-outline" size={20} color={cores.azul} /></View>
|
||||
<Text style={[styles.actionText, { color: cores.texto }]}>Redefinir Senha</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
<TouchableOpacity style={[styles.actionMenuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/redefenirsenha')}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.actionText, { color: cores.texto }]}>Alterar palavra-passe</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.borda} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.actionMenuItem, { backgroundColor: cores.card, marginTop: 12, marginBottom: 40 }]} onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: cores.vermelhoSuave }]}><Ionicons name="log-out-outline" size={20} color={cores.vermelho} /></View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionMenuItem, { backgroundColor: cores.card, marginTop: 12, marginBottom: 50 }]}
|
||||
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
|
||||
>
|
||||
<View style={[styles.actionIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.actionText, { color: cores.vermelho }]}>Terminar Sessão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -248,23 +334,15 @@ export default function PerfilAluno() {
|
||||
);
|
||||
}
|
||||
|
||||
const PerfilInput = ({ label, icon, cores, editable, multiline, ...props }: any) => (
|
||||
const PerfilInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={[styles.inputContainer, {
|
||||
backgroundColor: cores.fundo,
|
||||
borderColor: editable ? cores.azul : cores.borda,
|
||||
height: multiline ? undefined : 52,
|
||||
minHeight: 52,
|
||||
paddingVertical: multiline ? 8 : 0
|
||||
}]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginHorizontal: 12, marginTop: multiline ? 10 : 0 }} />
|
||||
<View style={[styles.inputContainer, { backgroundColor: cores.fundo, borderColor: editable ? cores.azul : cores.borda, borderStyle: editable ? 'solid' : 'dashed' }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginHorizontal: 12 }} />
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={editable}
|
||||
multiline={multiline}
|
||||
scrollEnabled={false}
|
||||
style={[styles.textInput, { color: cores.texto, textAlignVertical: multiline ? 'top' : 'center' }]}
|
||||
style={[styles.textInput, { color: cores.texto }]}
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
</View>
|
||||
@@ -275,26 +353,25 @@ const styles = StyleSheet.create({
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
headerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 19, fontWeight: '900' },
|
||||
roundBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 2 },
|
||||
scrollContainer: { paddingHorizontal: 20, paddingBottom: 20 },
|
||||
avatarSection: { alignItems: 'center', marginVertical: 25 },
|
||||
avatarFrame: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' },
|
||||
avatarCircle: { width: 95, height: 95, borderRadius: 48, alignItems: 'center', justifyContent: 'center', elevation: 5 },
|
||||
roundBtn: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
|
||||
scrollContainer: { paddingHorizontal: 20 },
|
||||
avatarSection: { alignItems: 'center', marginVertical: 20 },
|
||||
avatarCircle: { width: 100, height: 100, borderRadius: 50, alignItems: 'center', justifyContent: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
avatarInitial: { color: '#fff', fontSize: 40, fontWeight: '800' },
|
||||
profileName: { fontSize: 24, fontWeight: '900', marginTop: 15 },
|
||||
badge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
|
||||
badgeText: { fontSize: 11, fontWeight: '800', letterSpacing: 1 },
|
||||
sectionHeader: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginLeft: 8, marginBottom: 12, marginTop: 15 },
|
||||
infoCard: { borderRadius: 25, padding: 20, elevation: 3, shadowOpacity: 0.1 },
|
||||
profileName: { fontSize: 24, fontWeight: '900', marginTop: 15, textAlign: 'center', width: '100%' },
|
||||
badge: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 10, marginTop: 8 },
|
||||
badgeText: { fontSize: 10, fontWeight: '900', letterSpacing: 1, textTransform: 'uppercase' },
|
||||
sectionHeader: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginTop: 25, marginLeft: 5 },
|
||||
infoCard: { borderRadius: 24, padding: 20, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
inputGroup: { marginBottom: 15 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 16, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 14, fontWeight: '700' },
|
||||
inputLabel: { fontSize: 9, fontWeight: '900', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, borderWidth: 1.5, height: 52 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
|
||||
inputRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
footer: { marginTop: 20 },
|
||||
actionMenuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, elevation: 2 },
|
||||
actionIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
actionText: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '800' },
|
||||
alert: { position: 'absolute', left: 20, right: 20, padding: 18, borderRadius: 20, flexDirection: 'row', alignItems: 'center', zIndex: 999 },
|
||||
alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1 }
|
||||
miniStatus: { padding: 12, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
|
||||
miniStatusText: { fontSize: 10, fontWeight: '900' },
|
||||
footer: { marginTop: 25 },
|
||||
actionMenuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 3 },
|
||||
actionIcon: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
actionText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' }
|
||||
});
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- INTERFACES ---
|
||||
interface Presenca {
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// Função para calcular idade automaticamente
|
||||
const calcularIdade = (data: string): string => {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- UTILITÁRIOS ---
|
||||
const calcularIdade = (dataNascimento: string) => {
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- Interfaces ---
|
||||
interface Aluno { id: string; nome: string; turma_curso: string; ano: number; }
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- INTERFACES ---
|
||||
export interface Aluno {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- INTERFACES ---
|
||||
export interface Aluno {
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- INTERFACES ---
|
||||
export interface Aluno {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
// --- INTERFACES ---
|
||||
export interface Aluno {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
export interface Empresa {
|
||||
id: string;
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
export interface Empresa {
|
||||
id: number;
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
interface PerfilData {
|
||||
id: string;
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const Definicoes = memo(() => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../app/lib/supabase';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
// app/_layout.tsx
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { Stack } from 'expo-router';
|
||||
import { Stack, useRouter } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
// CORREÇÃO: O caminho mudou porque moveste a pasta lib para a raiz
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { ThemeProvider, useTheme } from '../themecontext';
|
||||
|
||||
function RootLayoutContent() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Escuta mudanças no estado de autenticação
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
|
||||
console.log("EVENTO SUPABASE:", event);
|
||||
|
||||
if (event === 'PASSWORD_RECOVERY') {
|
||||
console.log("A redirecionar para: /novapasse");
|
||||
|
||||
// Usamos um delay ligeiramente maior para garantir que o RootLayout
|
||||
// já montou a pilha de navegação (Stack)
|
||||
const timer = setTimeout(() => {
|
||||
// Usamos replace para não permitir que o user volte para uma rota "quebrada"
|
||||
router.replace('/novapasse');
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
authListener.subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusBar style={isDarkMode ? "light" : "dark"} />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
{/* Removido o .tsx do name, o Expo Router usa apenas o nome do ficheiro */}
|
||||
<Stack.Screen name="index" />
|
||||
{/* O index é o teu login */}
|
||||
<Stack.Screen name="index" />
|
||||
{/* Garantimos que a rota novapasse existe na stack */}
|
||||
<Stack.Screen name="novapasse" />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
@@ -23,10 +53,4 @@ export default function RootLayout() {
|
||||
<RootLayoutContent />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
<ThemeProvider>
|
||||
<NavigationContainer children={undefined}>
|
||||
{/* aluno e professor */}
|
||||
</NavigationContainer>
|
||||
</ThemeProvider>
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import Auth from '../components/Auth';
|
||||
import { supabase } from './lib/supabase';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { AppState, Platform } from 'react-native';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
|
||||
// Substitui pelas tuas credenciais se necessário (estas são as que enviaste)
|
||||
const supabaseUrl = 'https://ssorfpctjeujolmtkfib.supabase.co';
|
||||
const supabaseAnonKey = 'sb_publishable_SDocGprdYkUKi04FyfVqmA_Ykirp9cK';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
// No React Native usamos o AsyncStorage para persistir o login
|
||||
storage: AsyncStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
// Nota: Removido o 'lock' e o 'lockAcquireTimeout' para evitar erros de TS
|
||||
// O Supabase v2 no mobile já gere o fluxo de tokens de forma mais estável sem eles.
|
||||
},
|
||||
});
|
||||
|
||||
// Garante que o refresh do token só acontece quando a app está visível (Foreground)
|
||||
if (Platform.OS !== "web") {
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
supabase.auth.startAutoRefresh();
|
||||
} else {
|
||||
supabase.auth.stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
239
app/novapasse.tsx
Normal file
239
app/novapasse.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
// CORREÇÃO 1: O caminho agora é ../lib/supabase porque moveste a pasta para a raiz
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
// CORREÇÃO 2: O nome da função deve ser preferencialmente o nome do ficheiro
|
||||
export default function NovaPasse() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleUpdatePassword = async () => {
|
||||
setStatus(null);
|
||||
|
||||
if (password.length < 6) {
|
||||
setStatus({ type: 'error', msg: 'A password deve ter pelo menos 6 caracteres.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setStatus({ type: 'error', msg: 'As passwords não coincidem.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// O utilizador já está autenticado pelo link de recuperação
|
||||
const { error } = await supabase.auth.updateUser({ password: password });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setStatus({ type: 'success', msg: 'Sucesso! A tua password foi atualizada.' });
|
||||
|
||||
// Manda para o ecrã inicial (index) após 3 segundos
|
||||
setTimeout(() => router.replace('/'), 3000);
|
||||
|
||||
} catch (err: any) {
|
||||
setStatus({ type: 'error', msg: 'Erro ao atualizar. O link pode ter expirado.' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.mainContainer}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<View style={styles.iconCircle}>
|
||||
<Ionicons name="lock-open-outline" size={38} color="#2390a6" />
|
||||
</View>
|
||||
<View style={styles.iconBadge} />
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>Nova Palavra-passe</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Cria uma nova senha segura para acederes à tua conta.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{status && (
|
||||
<View style={[styles.statusBanner, status.type === 'success' ? styles.successBg : styles.errorBg]}>
|
||||
<Ionicons
|
||||
name={status.type === 'success' ? "checkmark-circle" : "alert-circle"}
|
||||
size={20}
|
||||
color={status.type === 'success' ? "#059669" : "#EF4444"}
|
||||
/>
|
||||
<Text style={[styles.statusText, status.type === 'success' ? styles.successText : styles.errorText]}>
|
||||
{status.msg}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.label, isFocused && { color: '#2390a6' }]}>Nova Password</Text>
|
||||
<TextInput
|
||||
style={[styles.input, isFocused && styles.inputFocused]}
|
||||
placeholder="Mínimo 6 caracteres"
|
||||
placeholderTextColor="#94A3B8"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={styles.label}>Confirmar Password</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Repita a password"
|
||||
placeholderTextColor="#94A3B8"
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleUpdatePassword}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Atualizar Password</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Segurança EPVC Estágios+ • 2026</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContainer: { flex: 1, backgroundColor: '#FFFFFF' },
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 28,
|
||||
paddingTop: 80,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
content: { flex: 1, justifyContent: 'center' },
|
||||
header: { alignItems: 'center', marginBottom: 35 },
|
||||
iconWrapper: { position: 'relative', marginBottom: 20 },
|
||||
iconCircle: {
|
||||
width: 90,
|
||||
height: 90,
|
||||
backgroundColor: '#F0F7FF',
|
||||
borderRadius: 30,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#DBEAFE',
|
||||
},
|
||||
iconBadge: {
|
||||
position: 'absolute',
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#2390a6',
|
||||
borderWidth: 3,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
color: '#0F172A',
|
||||
textAlign: 'center',
|
||||
letterSpacing: -1,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
color: '#64748B',
|
||||
textAlign: 'center',
|
||||
marginTop: 10,
|
||||
lineHeight: 22,
|
||||
maxWidth: 300,
|
||||
},
|
||||
statusBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 25,
|
||||
borderWidth: 1,
|
||||
},
|
||||
statusText: { fontSize: 14, fontWeight: '600', marginLeft: 10, flex: 1 },
|
||||
errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' },
|
||||
errorText: { color: '#B91C1C' },
|
||||
successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' },
|
||||
successText: { color: '#166534' },
|
||||
form: { width: '100%' },
|
||||
inputWrapper: { marginBottom: 24 },
|
||||
label: { fontSize: 14, fontWeight: '700', color: '#475569', marginBottom: 10, marginLeft: 4 },
|
||||
input: {
|
||||
backgroundColor: '#FBFDFF',
|
||||
borderRadius: 18,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
fontSize: 16,
|
||||
color: '#0F172A',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#F1F5F9',
|
||||
},
|
||||
inputFocused: { borderColor: '#2390a6', backgroundColor: '#FFFFFF' },
|
||||
button: {
|
||||
backgroundColor: '#dd8707',
|
||||
borderRadius: 18,
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
elevation: 6,
|
||||
},
|
||||
buttonDisabled: { backgroundColor: '#E2E8F0', elevation: 0 },
|
||||
buttonText: { color: '#fff', fontSize: 16, fontWeight: '800' },
|
||||
footer: { marginTop: 40, alignItems: 'center' },
|
||||
footerText: { fontSize: 12, color: '#CBD5E1', fontWeight: '600' },
|
||||
});
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { supabase } from '../app/lib/supabase';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
Reference in New Issue
Block a user