primeiro commit
This commit is contained in:
14
app/(tabs)/_layout.tsx
Normal file
14
app/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
initialRouteName="inicio" // Define qual o nome do ficheiro que abre primeiro
|
||||
screenOptions={{ headerShown: false }}
|
||||
>
|
||||
<Tabs.Screen name="index" options={{ title: 'Login' }} />
|
||||
<Tabs.Screen name="inicio" options={{ title: 'Início' }} />
|
||||
<Tabs.Screen name="registo" options={{ title: 'Registo' }} />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
123
app/(tabs)/esqueciPalavraPasse.tsx
Normal file
123
app/(tabs)/esqueciPalavraPasse.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Colors } from '../../src/components/common/theme';
|
||||
|
||||
export default function EsqueciPalavraPasse() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleReset = () => {
|
||||
if (!email) {
|
||||
Alert.alert('Erro', 'Por favor insira o seu email');
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
// simulação de envio de instruções
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
Alert.alert('Sucesso', 'Um email com instruções foi enviado.');
|
||||
// o TypeScript não reconhece "/inicio" nas rotas tipadas; forçamos o cast
|
||||
router.replace('/inicio' as any);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<Text style={styles.title}>Fluxup</Text>
|
||||
<Text style={styles.subtitle}>Recuperar palavra‑passe</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#999"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleReset}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Enviar</Text>}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Text style={styles.linkText}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const primary = Colors.light.tint;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.light.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: Colors.light.background,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: primary,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: Colors.light.icon,
|
||||
marginBottom: 24,
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
paddingHorizontal: 15,
|
||||
marginBottom: 15,
|
||||
fontSize: 16,
|
||||
backgroundColor: '#f9f9f9',
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
backgroundColor: primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#ccc',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
linkText: {
|
||||
color: primary,
|
||||
fontSize: 14,
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
219
app/(tabs)/index.tsx
Normal file
219
app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { usuariosService } from '../../src/lib/supabase';
|
||||
import { Colors } from '../../src/components/common/theme';
|
||||
import { useColorScheme } from '../../src/hooks/use-color-scheme';
|
||||
import { Lock, Mail } from 'lucide-react-native';
|
||||
import { Input } from '../../src/components/common/input';
|
||||
import { Button } from '../../src/components/common/button';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [palavraPasse, setPalavraPasse] = useState('');
|
||||
const [idUsuario, setIdUsuario] = useState('');
|
||||
const [usuario, setUsuario] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = colorScheme === 'dark' ? 'dark' : 'light';
|
||||
const primary = Colors[theme].primary;
|
||||
|
||||
const handleLogin = async () => {
|
||||
// Validação dos campos
|
||||
if (!email || !palavraPasse || !idUsuario || !usuario) {
|
||||
Alert.alert('Erro', 'Por favor, preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validação de email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
Alert.alert('Erro', 'Email inválido');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// login no Supabase
|
||||
const { data, error } = await usuariosService.login(email, palavraPasse);
|
||||
|
||||
if (error) {
|
||||
Alert.alert('Erro', 'Email ou palavra passe incorretos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
Alert.alert('Sucesso', `Bem-vindo, ${(data as any).usuario || 'usuário'}!`);
|
||||
// quando o login é bem‑sucedido, vai para a página inicial
|
||||
router.replace('/inicio');
|
||||
}
|
||||
} catch (err) {
|
||||
Alert.alert('Erro', 'Erro ao conectar ao servidor');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: Colors[theme].background }]}>
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.container, { backgroundColor: Colors[theme].background }]}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<Text style={[styles.title, { color: primary } ]}>Fluxup</Text>
|
||||
<Text style={[styles.subtitle, { color: Colors[theme].textMuted } ]}>Entre na sua conta</Text>
|
||||
|
||||
{/* Campo de Email */}
|
||||
<View style={[styles.inputContainer, { backgroundColor: Colors[theme].card, borderColor: Colors[theme].border }] }>
|
||||
<Mail size={20} color={Colors[theme].icon} style={styles.icon} />
|
||||
<Input
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
editable={!loading}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Campo de Palavra Passe */}
|
||||
<View style={[styles.inputContainer, { backgroundColor: Colors[theme].card, borderColor: Colors[theme].border }] }>
|
||||
<Lock size={20} color={Colors[theme].icon} style={styles.icon} />
|
||||
<Input
|
||||
placeholder="Palavra Passe"
|
||||
value={palavraPasse}
|
||||
onChangeText={setPalavraPasse}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Campo de ID Usuário */}
|
||||
<Input
|
||||
placeholder="ID Usuário"
|
||||
value={idUsuario}
|
||||
onChangeText={setIdUsuario}
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* Campo de Usuário */}
|
||||
<Input
|
||||
placeholder="Usuário"
|
||||
value={usuario}
|
||||
onChangeText={setUsuario}
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* Botão de Login */}
|
||||
<Button
|
||||
title="Entrar"
|
||||
onPress={handleLogin}
|
||||
isLoading={loading}
|
||||
disabled={loading}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
containerStyle={styles.button}
|
||||
textStyle={styles.buttonText}
|
||||
/>
|
||||
|
||||
<View style={styles.footerLinks}>
|
||||
{/* Botão de Registo */}
|
||||
<TouchableOpacity onPress={() => router.push('/registo')}>
|
||||
<Text style={[styles.linkText, { color: primary }]}>
|
||||
Não tem conta? <Text style={[styles.linkBold, { color: primary } ]}>Registe-se</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Botão de Esqueci Palavra Passe */}
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/esqueciPalavraPasse')}
|
||||
>
|
||||
<Text style={[styles.linkText, { color: primary }]}>Esqueci a palavra‑passe</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
marginBottom: 15,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#ccc',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
linkBold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
footerLinks: {
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
349
app/(tabs)/inicio.tsx
Normal file
349
app/(tabs)/inicio.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import {
|
||||
Home,
|
||||
FileText,
|
||||
BarChart2,
|
||||
Settings,
|
||||
Star,
|
||||
} from 'lucide-react-native';
|
||||
|
||||
const PURPLE = '#6a00fa';
|
||||
const CARD_BG = '#fff';
|
||||
|
||||
export default function InicioScreen() {
|
||||
const router = useRouter();
|
||||
|
||||
// --- authentication guard (placeholder) ---
|
||||
useEffect(() => {
|
||||
const isAuthenticated = true; // trocar por verificação real
|
||||
if (!isAuthenticated) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
// --- checklist state ---
|
||||
const [tasks, setTasks] = useState([
|
||||
{ id: 1, label: 'Estudar para intermédio', done: false },
|
||||
{ id: 2, label: 'Ler artigo técnico', done: false },
|
||||
{ id: 3, label: 'Fazer exercícios', done: false },
|
||||
]);
|
||||
const toggleTask = (id: number) => {
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
|
||||
);
|
||||
};
|
||||
|
||||
// --- habits rating ---
|
||||
const [rating, setRating] = useState(4);
|
||||
|
||||
// --- pomodoro timer ---
|
||||
const [secondsLeft, setSecondsLeft] = useState(25 * 60);
|
||||
const [running, setRunning] = useState(false);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (running) {
|
||||
const id = setInterval(() => {
|
||||
setSecondsLeft((s) => {
|
||||
if (s <= 1) {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
setRunning(false);
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000) as unknown as number;
|
||||
intervalRef.current = id;
|
||||
} else {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [running]);
|
||||
|
||||
const startTimer = () => {
|
||||
if (!running) {
|
||||
setSecondsLeft(25 * 60);
|
||||
setRunning(true);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (sec: number) => {
|
||||
const m = Math.floor(sec / 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const s = (sec % 60).toString().padStart(2, '0');
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
{/* header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Hoje é um bom dia</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Olá, Matheus! Preparado para o seu dia de produtividade?
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.waveEmoji}>👋</Text>
|
||||
</View>
|
||||
|
||||
{/* desafios do dia card */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Desafios do Dia</Text>
|
||||
{tasks.map((t) => (
|
||||
<TouchableOpacity
|
||||
key={t.id}
|
||||
style={styles.checkItem}
|
||||
onPress={() => toggleTask(t.id)}
|
||||
>
|
||||
<Text style={[styles.checkBox, t.done && styles.done]}>✓</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.checkLabel,
|
||||
t.done && styles.checkLabelDone,
|
||||
]}
|
||||
>
|
||||
{t.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => Alert.alert('Adicionar','implementação futura')}
|
||||
>
|
||||
<Text style={styles.addButtonText}>+ Adicionar desafios diários</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* hábitos card */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Hábitos</Text>
|
||||
<View style={styles.ratingRow}>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TouchableOpacity key={i} onPress={() => setRating(i + 1)} activeOpacity={0.7}>
|
||||
<Star
|
||||
size={20}
|
||||
stroke={i < rating ? '#f5c518' : '#ccc'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.habitsRow}>
|
||||
<TouchableOpacity style={[styles.habitButton, { backgroundColor: '#A0E9FD' }]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text>📘</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.habitButton, { backgroundColor: '#B2F59C' }]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text>🧘</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.habitButton, { backgroundColor: '#FFD59C' }]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text>🏋️♂️</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.habitButton, styles.habitAdd]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* modo foco card */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Modo Foco</Text>
|
||||
<View style={styles.focusRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.timerBlock}
|
||||
onPress={startTimer}
|
||||
>
|
||||
<Text style={styles.timerText}>{formatTime(secondsLeft)}</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.focusText}>
|
||||
Produtividade é o segredo do sucesso
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* bottom navigation */}
|
||||
<View style={styles.bottomNav}>
|
||||
<TouchableOpacity style={styles.navItem} disabled>
|
||||
<Home size={24} stroke={PURPLE} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<FileText size={24} stroke="#666" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<BarChart2 size={24} stroke="#666" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.navItem}>
|
||||
<Settings size={24} stroke="#666" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
backgroundColor: PURPLE,
|
||||
},
|
||||
scroll: {
|
||||
padding: 20,
|
||||
paddingBottom: 80,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
greeting: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#eee',
|
||||
marginTop: 4,
|
||||
},
|
||||
waveEmoji: {
|
||||
fontSize: 28,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: CARD_BG,
|
||||
borderRadius: 24,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 12,
|
||||
},
|
||||
checkItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
checkBox: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: '#666',
|
||||
marginRight: 8,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
},
|
||||
done: {
|
||||
backgroundColor: '#6a0fa0',
|
||||
color: '#fff',
|
||||
borderColor: '#6a0fa0',
|
||||
},
|
||||
checkLabel: {
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
},
|
||||
checkLabelDone: {
|
||||
textDecorationLine: 'line-through',
|
||||
color: '#999',
|
||||
},
|
||||
addButton: {
|
||||
marginTop: 12,
|
||||
backgroundColor: PURPLE,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
addButtonText: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
ratingRow: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 12,
|
||||
},
|
||||
habitsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
habitButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
habitAdd: {
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: '#666',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
focusRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
timerBlock: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: '#3b44f6',
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
timerText: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
focusText: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
bottomNav: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 60,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
backgroundColor: CARD_BG,
|
||||
borderTopWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
},
|
||||
navItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
29
app/(tabs)/modal.tsx
Normal file
29
app/(tabs)/modal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ThemedText } from '../../src/components/common/themed-text';
|
||||
import { ThemedView } from '../../src/components/common/themed-view';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This is a modal</ThemedText>
|
||||
<Link href="/" dismissTo style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
300
app/(tabs)/registo.tsx
Normal file
300
app/(tabs)/registo.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
} from 'react-native';
|
||||
import { useRouter, Link } from 'expo-router';
|
||||
import { User, Mail, Lock, Phone, ArrowLeft, CheckCircle } from 'lucide-react-native';
|
||||
import { Colors } from '../../src/components/common/theme';
|
||||
|
||||
// import { supabase } from '../lib/supabase';
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const router = useRouter();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleRegister() {
|
||||
// 1. Validação simples antes de enviar
|
||||
if (password !== confirmPassword) {
|
||||
Alert.alert('Erro', 'As palavras-passe não coincidem.');
|
||||
return;
|
||||
}
|
||||
if (username.length < 3) {
|
||||
Alert.alert('Erro', 'O nome de utilizador deve ter pelo menos 3 caracteres.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// Simulação de registo
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
Alert.alert('Sucesso', 'Conta criada com sucesso! A entrar...');
|
||||
// em vez de voltar ao login, encaminhamos directamente para a tela inicial
|
||||
router.replace('/inicio' as any);
|
||||
}, 1500);
|
||||
|
||||
/* LÓGICA REAL DO SUPABASE (Para depois):
|
||||
|
||||
// 1. Criar utilizador na Auth
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
Alert.alert('Erro', authError.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Guardar dados extra na tabela 'profiles'
|
||||
if (authData.user) {
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.insert([{
|
||||
id: authData.user.id,
|
||||
username: username,
|
||||
phone_number: phone,
|
||||
full_name: username, // Podes mudar isto depois
|
||||
updated_at: new Date(),
|
||||
}]);
|
||||
|
||||
if (profileError) {
|
||||
Alert.alert('Aviso', 'Conta criada, mas houve um erro ao salvar o perfil.');
|
||||
} else {
|
||||
Alert.alert('Sucesso', 'Verifica o teu email para confirmar a conta!');
|
||||
router.replace('/inicio');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
*/
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
|
||||
{/* Botão Voltar */}
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<ArrowLeft size={24} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Fluxup</Text>
|
||||
<Text style={styles.subtitle}>Registar nova conta</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
|
||||
{/* Nome de Utilizador */}
|
||||
<View style={styles.inputContainer}>
|
||||
<User size={20} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Nome de utilizador"
|
||||
placeholderTextColor="#999"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Email */}
|
||||
<View style={styles.inputContainer}>
|
||||
<Mail size={20} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#999"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Telemóvel */}
|
||||
<View style={styles.inputContainer}>
|
||||
<Phone size={20} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Número de telemóvel"
|
||||
placeholderTextColor="#999"
|
||||
value={phone}
|
||||
onChangeText={setPhone}
|
||||
keyboardType="phone-pad"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Password */}
|
||||
<View style={styles.inputContainer}>
|
||||
<Lock size={20} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Palavra-passe"
|
||||
placeholderTextColor="#999"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Confirmar Password */}
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
// Muda a cor da borda se as senhas não baterem (e se já tiver escrito algo)
|
||||
confirmPassword.length > 0 && password !== confirmPassword ? { borderColor: '#EF4444' } : {}
|
||||
]}>
|
||||
<Lock size={20} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirmar palavra-passe"
|
||||
placeholderTextColor="#999"
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
{/* Ícone de verificação verde se as senhas baterem */}
|
||||
{password.length > 0 && password === confirmPassword && (
|
||||
<CheckCircle size={20} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleRegister}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Registar</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Já tens conta? </Text>
|
||||
<Link href="/" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={styles.linkText}>Entrar</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const primary = Colors.light.tint;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.light.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.light.background,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
padding: 24,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
backButton: {
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
header: {
|
||||
marginBottom: 30,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: primary,
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: Colors.light.icon,
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: 56,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
color: Colors.light.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: primary,
|
||||
height: 56,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
shadowColor: primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 24,
|
||||
marginBottom: 20,
|
||||
},
|
||||
footerText: {
|
||||
color: Colors.light.icon,
|
||||
fontSize: 16,
|
||||
},
|
||||
linkText: {
|
||||
color: primary,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user