26.1.20
This commit is contained in:
151
app/index.tsx
151
app/index.tsx
@@ -1,145 +1,36 @@
|
||||
|
||||
|
||||
// app/index.tsx - TELA DE LOGIN
|
||||
|
||||
|
||||
import { Link, useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
// app/index.tsx
|
||||
import { useRouter } from 'expo-router';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import Auth from '../components/Auth';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const router = useRouter(); // Inicializa o router
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Atenção', 'Por favor, preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
Alert.alert('Email inválido', 'Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// SIMULAÇÃO DE LOGIN
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
|
||||
// Primeiro navega para a dashboard
|
||||
router.replace('/AlunoHome'); // ⬅️ Certifica-te que o ficheiro é app/dashboard.tsx
|
||||
|
||||
// Depois mostra alert de boas-vindas (opcional)
|
||||
setTimeout(() => {
|
||||
Alert.alert('Login realizado!', `Bem-vindo(a), ${email.split('@')[0]}!`);
|
||||
}, 300); // delay pequeno para garantir que a navegação ocorreu
|
||||
|
||||
}, 1500);
|
||||
const handleLoginSuccess = () => {
|
||||
router.replace('/AlunoHome');
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
|
||||
{/* LOGO/TÍTULO */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>📱 Estágios+</Text>
|
||||
<Text style={styles.subtitle}>Escola Profissional de Vila do Conde</Text>
|
||||
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: '#f8f9fa' }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>📱 Estágios+</Text>
|
||||
<Text style={styles.subtitle}>Escola Profissional de Vila do Conde</Text>
|
||||
</View>
|
||||
|
||||
{/* Componente Auth */}
|
||||
<Auth onLoginSuccess={handleLoginSuccess} />
|
||||
</View>
|
||||
|
||||
{/* FORMULÁRIO */}
|
||||
<View style={styles.form}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu email"
|
||||
placeholderTextColor="#999"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Palavra-passe</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira a sua palavra-passe"
|
||||
placeholderTextColor="#999"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* BOTÃO ENTRAR */}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>ENTRAR</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* LINK ESQUECI SENHA */}
|
||||
<TouchableOpacity style={styles.forgotLink}>
|
||||
<Text style={styles.forgotText}>Esqueceu-se da palavra-passe?</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* CADASTRO */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Não tem uma conta?</Text>
|
||||
<Link href="/register" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={styles.registerText}> Crie uma conta agora</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
// ESTILOS
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f8f9fa' },
|
||||
content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
|
||||
scrollContainer: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 40 },
|
||||
content: { flex: 1, justifyContent: 'center' },
|
||||
header: { alignItems: 'center', marginBottom: 48 },
|
||||
title: { fontSize: 32, fontWeight: '800', color: '#2d3436', marginBottom: 8 },
|
||||
subtitle: { fontSize: 16, color: '#636e72', textAlign: 'center' },
|
||||
form: { backgroundColor: '#fff', borderRadius: 20, padding: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 5 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8, marginLeft: 4 },
|
||||
input: { backgroundColor: '#f8f9fa', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, marginBottom: 20, borderWidth: 1, borderColor: '#dfe6e9', color: '#2d3436' },
|
||||
button: { backgroundColor: '#0984e3', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 8, marginBottom: 24 },
|
||||
buttonDisabled: { backgroundColor: '#74b9ff' },
|
||||
buttonText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
||||
forgotLink: { alignItems: 'center' },
|
||||
forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500' },
|
||||
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 40, paddingTop: 24, borderTopWidth: 1, borderTopColor: '#dfe6e9' },
|
||||
footerText: { color: '#636e72', fontSize: 15 },
|
||||
registerText: { color: '#0984e3', fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
});
|
||||
|
||||
32
app/lib/supabase.ts
Normal file
32
app/lib/supabase.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { createClient, processLock } from '@supabase/supabase-js'
|
||||
import { AppState, Platform } from 'react-native'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
const supabaseUrl = 'https://ssorfpctjeujolmtkfib.supabase.co'
|
||||
const supabaseAnonKey = 'sb_publishable_SDocGprdYkUKi04FyfVqmA_Ykirp9cK'
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
...(Platform.OS !== "web" ? { storage: AsyncStorage } : {}),
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
lock: processLock,
|
||||
},
|
||||
})
|
||||
|
||||
// Tells Supabase Auth to continuously refresh the session automatically
|
||||
// if the app is in the foreground. When this is added, you will continue
|
||||
// to receive `onAuthStateChange` events with the `TOKEN_REFRESHED` or
|
||||
// `SIGNED_OUT` event if the user's session is terminated. This should
|
||||
// only be registered once.
|
||||
if (Platform.OS !== "web") {
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
supabase.auth.startAutoRefresh()
|
||||
} else {
|
||||
supabase.auth.stopAutoRefresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
131
app/redefenirsenha.tsx
Normal file
131
app/redefenirsenha.tsx
Normal 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 '../app/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('/')} style={styles.backContainer}>
|
||||
<Text style={styles.backText}>← Voltar para Login</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' },
|
||||
});
|
||||
312
app/register.tsx
312
app/register.tsx
@@ -1,312 +0,0 @@
|
||||
|
||||
|
||||
// app/register.tsx - TELA DE CRIAR CONTA
|
||||
|
||||
|
||||
import { Link } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function CriarContaScreen() {
|
||||
const [form, setForm] = useState({
|
||||
nome: '',
|
||||
email: '',
|
||||
telefone: '',
|
||||
password: '',
|
||||
confirmarPassword: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!form.nome.trim()) {
|
||||
Alert.alert('Erro', 'Por favor, insira o seu nome');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.email.includes('@')) {
|
||||
Alert.alert('Erro', 'Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 6) {
|
||||
Alert.alert('Erro', 'A senha deve ter pelo menos 6 caracteres');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password !== form.confirmarPassword) {
|
||||
Alert.alert('Erro', 'As senhas não coincidem');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
Alert.alert(
|
||||
'Sucesso!',
|
||||
`Conta criada para ${form.nome}`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
|
||||
setForm({
|
||||
nome: '',
|
||||
email: '',
|
||||
telefone: '',
|
||||
password: '',
|
||||
confirmarPassword: ''
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* BOTÃO VOLTAR ATRÁS */}
|
||||
<Link href={"/"} asChild>
|
||||
<TouchableOpacity style={styles.backHeaderButton} disabled={loading}>
|
||||
<Text style={styles.backHeaderText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
|
||||
{/* CABEÇALHO */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Criar Nova Conta</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Preencha os dados abaixo para se registar
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* FORMULÁRIO */}
|
||||
<View style={styles.formCard}>
|
||||
{/* NOME COMPLETO */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Nome Completo</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu nome completo..."
|
||||
value={form.nome}
|
||||
onChangeText={(text) => handleChange('nome', text)}
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* EMAIL */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu email..."
|
||||
value={form.email}
|
||||
onChangeText={(text) => handleChange('email', text)}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Nº TELEMÓVEL */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Telefone</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu nº telemóvel..."
|
||||
value={form.telefone}
|
||||
onChangeText={(text) => handleChange('telefone', text)}
|
||||
keyboardType="phone-pad"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* PALAVRA-PASSE */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Senha</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Mínimo de 6 caracteres"
|
||||
value={form.password}
|
||||
onChangeText={(text) => handleChange('password', text)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* CONFIRMAR PALAVRA-PASSE */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Confirmar Senha</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira novamente a sua palavra-passe"
|
||||
value={form.confirmarPassword}
|
||||
onChangeText={(text) => handleChange('confirmarPassword', text)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* BOTÃO CRIAR CONTA */}
|
||||
<TouchableOpacity
|
||||
style={[styles.registerButton, loading && styles.buttonDisabled]}
|
||||
onPress={handleRegister}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" />
|
||||
) : (
|
||||
<Text style={styles.registerButtonText}>CRIAR CONTA</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* AVISO */}
|
||||
<View style={styles.termsContainer}>
|
||||
<Text style={styles.termsText}>
|
||||
Ao criar uma conta, concorda com os nossos Termos de Serviço e Política de Privacidade.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// ESTILOS
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
padding: 20,
|
||||
paddingTop: 20,
|
||||
},
|
||||
backHeaderButton: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 10,
|
||||
zIndex: 50,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#f0f0f0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backHeaderText: {
|
||||
fontSize: 24,
|
||||
color: '#007AFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginTop: 50,
|
||||
marginBottom: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#1a1a1a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
formCard: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
marginBottom: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e9ecef',
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 6,
|
||||
marginLeft: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
color: '#333',
|
||||
},
|
||||
registerButton: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#7bb8ff',
|
||||
},
|
||||
registerButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
termsContainer: {
|
||||
backgroundColor: '#e8f4ff',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#cce5ff',
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 13,
|
||||
color: '#0066cc',
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
},
|
||||
backButton: {
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
backButtonText: {
|
||||
color: '#007AFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
0
assets/images/logo.png
Normal file
0
assets/images/logo.png
Normal file
101
components/Auth.tsx
Normal file
101
components/Auth.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useRouter } from 'expo-router'; // IMPORTAR ROUTER
|
||||
import { useState } from 'react';
|
||||
import { ActivityIndicator, Alert, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { supabase } from '../app/lib/supabase';
|
||||
|
||||
interface AuthProps {
|
||||
onLoginSuccess?: () => void;
|
||||
}
|
||||
|
||||
export default function Auth({ onLoginSuccess }: AuthProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter(); // INICIALIZA O ROUTER
|
||||
|
||||
// LOGIN
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Atenção', 'Preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) throw error;
|
||||
|
||||
Alert.alert('Bem-vindo(a)!');
|
||||
if (onLoginSuccess) onLoginSuccess();
|
||||
} catch (err: any) {
|
||||
Alert.alert('Erro', err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.form}>
|
||||
{/* EMAIL */}
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="email@address.com"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* PASSWORD */}
|
||||
<Text style={styles.label}>Palavra-passe</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira a sua palavra-passe"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* BOTÃO ENTRAR MODERNO */}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>ENTRAR</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* TEXTO DE ESQUECI A SENHA → NAVEGA */}
|
||||
<TouchableOpacity onPress={() => router.push('/redefenirsenha')}>
|
||||
<Text style={styles.forgotText}>Esqueceu a palavra-passe?</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
form: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
marginTop: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 5,
|
||||
},
|
||||
label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8 },
|
||||
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' },
|
||||
forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500', textAlign: 'center', marginTop: 8 },
|
||||
});
|
||||
145
package-lock.json
generated
145
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"expo": "~54.0.27",
|
||||
"expo-constants": "~18.0.11",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
@@ -35,6 +36,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
@@ -3463,6 +3465,107 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.91.0.tgz",
|
||||
"integrity": "sha512-9ywvsKLsxTwv7fvN5fXzP3UfRreqrX2waylTBDu0lkmeHXa8WtSQS9e0WV9FBduiazYqQbgfBQXBNPRPsRgWOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.91.0.tgz",
|
||||
"integrity": "sha512-WaakXOqLK1mLtBNFXp5o5T+LlI6KZuADSeXz+9ofPRG5OpVSvW148LVJB1DRZ16Phck1a0YqIUswOUgxCz6vMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.91.0.tgz",
|
||||
"integrity": "sha512-5S41zv2euNpGucvtM4Wy+xOmLznqt/XO+Lh823LOFEQ00ov7QJfvqb6VzIxufvzhooZpmGR0BxvMcJtWxCIFdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.91.0.tgz",
|
||||
"integrity": "sha512-u2YuJFG35umw8DO9beC27L/jYXm3KhF+73WQwbynMpV0tXsFIA0DOGRM0NgRyy03hJIdO6mxTTwe8efW3yx3Tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js/node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.91.0.tgz",
|
||||
"integrity": "sha512-CI7fsVIBQHfNObqU9kmyQ1GWr+Ug44y4rSpvxT4LdQB9tlhg1NTBov6z7Dlmt8d6lGi/8a9lf/epCDxyWI792g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iceberg-js": "^0.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.91.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.91.0.tgz",
|
||||
"integrity": "sha512-Rjb0QqkKrmXMVwUOdEqysPBZ0ZDZakeptTkUa6k2d8r3strBdbWVDqjOdkCjAmvvZMtXecBeyTyMEXD1Zzjfvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.91.0",
|
||||
"@supabase/functions-js": "2.91.0",
|
||||
"@supabase/postgrest-js": "2.91.0",
|
||||
"@supabase/realtime-js": "2.91.0",
|
||||
"@supabase/storage-js": "2.91.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
@@ -3584,6 +3687,12 @@
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||
@@ -3600,6 +3709,15 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -7709,6 +7827,15 @@
|
||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/iceberg-js": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -10703,6 +10830,18 @@
|
||||
"integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-url-polyfill": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
|
||||
"integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url-without-unicode": "8.0.0-3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||
@@ -11994,9 +12133,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
|
||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.4.tgz",
|
||||
"integrity": "sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"expo": "~54.0.27",
|
||||
"expo-constants": "~18.0.11",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
@@ -38,6 +39,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user