primeiro commit

This commit is contained in:
2026-04-15 12:46:50 +01:00
commit fa1accd4bb
961 changed files with 124535 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,2 @@
#Wed Mar 25 11:05:18 WET 2026
gradle.version=9.3.1

Binary file not shown.

View File

@@ -0,0 +1,2 @@
#Wed Mar 18 09:24:04 WET 2026
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View File

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
Fluxup

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

1610
.idea/caches/deviceStreaming.xml generated Normal file

File diff suppressed because it is too large Load Diff

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

11
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/studiobot.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

14
app/(tabs)/_layout.tsx Normal file
View 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>
);
}

View 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 palavrapasse</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
View 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 é bemsucedido, 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 palavrapasse</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
View 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
View 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
View 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}> 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,
},
});

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,2 @@
#Tue Mar 17 15:42:21 WET 2026
gradle.version=8.9

View File

58
app/build.gradle Normal file
View File

@@ -0,0 +1,58 @@
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
android {
namespace 'com.fluxup.app'
compileSdk 35
defaultConfig {
applicationId "com.fluxup.app"
minSdk 24
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment:2.6.0'
implementation 'androidx.navigation:navigation-ui:2.6.0'
// Align Kotlin versions
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
// Supabase dependencies (Placeholder for actual SDK or Retrofit)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-auth'
implementation 'com.google.firebase:firebase-firestore'
implementation 'androidx.credentials:credentials:1.6.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
apply plugin: 'com.google.gms.google-services'
}

BIN
app/build/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="gcm_defaultSenderId" translatable="false">659566714595</string>
<string name="google_api_key" translatable="false">AIzaSyDzpQM5ewUlGCb1gROzSxBXJYavyDrm1kk</string>
<string name="google_app_id" translatable="false">1:659566714595:android:e2dba99e1738528d966fd6</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyDzpQM5ewUlGCb1gROzSxBXJYavyDrm1kk</string>
<string name="google_storage_bucket" translatable="false">fluxupp.firebasestorage.app</string>
<string name="project_id" translatable="false">fluxupp</string>
</resources>

1
app/build/gmpAppId.txt Normal file
View File

@@ -0,0 +1 @@
1:659566714595:android:e2dba99e1738528d966fd6

View File

@@ -0,0 +1,9 @@
com.fluxup.app-pngs-0 /Users/230407/Desktop/FluxupP/app/build/generated/res/pngs/debug
com.fluxup.app-res-1 /Users/230407/Desktop/FluxupP/app/build/generated/res/processDebugGoogleServices
com.fluxup.app-resValues-2 /Users/230407/Desktop/FluxupP/app/build/generated/res/resValues/debug
com.fluxup.app-updated_navigation_xml-3 /Users/230407/Desktop/FluxupP/app/build/generated/updated_navigation_xml/debug
com.fluxup.app-packageDebugResources-4 /Users/230407/Desktop/FluxupP/app/build/intermediates/incremental/debug/packageDebugResources/merged.dir
com.fluxup.app-packageDebugResources-5 /Users/230407/Desktop/FluxupP/app/build/intermediates/incremental/debug/packageDebugResources/stripped.dir
com.fluxup.app-debug-6 /Users/230407/Desktop/FluxupP/app/build/intermediates/merged_res/debug/mergeDebugResources
com.fluxup.app-debug-7 /Users/230407/Desktop/FluxupP/app/src/debug/res
com.fluxup.app-main-8 /Users/230407/Desktop/FluxupP/app/src/main/res

Binary file not shown.

View File

@@ -0,0 +1,21 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.fluxup.app",
"variantName": "debug",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-debug.apk"
}
],
"elementType": "File",
"minSdkVersionForDexing": 24
}

View File

@@ -0,0 +1,2 @@
#- File Locator -
listingFile=../../../apk/debug/output-metadata.json

View File

@@ -0,0 +1,2 @@
#- File Locator -
listingFile=../../apk/debug/output-metadata.json

View File

@@ -0,0 +1,2 @@
appMetadataVersion=1.1
androidGradlePluginVersion=8.2.0

View File

@@ -0,0 +1,2 @@
appMetadataVersion=1.1
androidGradlePluginVersion=9.1.1

View File

@@ -0,0 +1,10 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "com.fluxup.app",
"variantName": "debug",
"elements": []
}

View File

@@ -0,0 +1,10 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "com.fluxup.app",
"variantName": "debug",
"elements": []
}

Some files were not shown because too many files have changed in this diff Show More