Versão 26.01.10 - Atualizações

This commit is contained in:
2026-01-10 00:34:54 +00:00
commit e831a216c1
42 changed files with 15784 additions and 0 deletions

294
app/AlunoHome.tsx Normal file
View File

@@ -0,0 +1,294 @@
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useFocusEffect } from '@react-navigation/native';
import * as DocumentPicker from 'expo-document-picker';
import { useRouter } from 'expo-router';
import * as Sharing from 'expo-sharing';
import React, { memo, useCallback, useMemo, useState } from 'react';
import {
Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
} from 'react-native';
import { Calendar, LocaleConfig } from 'react-native-calendars';
import { useTheme } from '../themecontext';
// Configuração PT
LocaleConfig.locales['pt'] = {
monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
dayNames: ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'],
dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
};
LocaleConfig.defaultLocale = 'pt';
// --- FUNÇÃO PARA CALCULAR FERIADOS (Nacionais + Vila do Conde) ---
const getFeriadosMap = (ano: number) => {
const f: Record<string, string> = {
[`${ano}-01-01`]: "Ano Novo",
[`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador",
[`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)", // Feriado Municipal
[`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Implantação da República",
[`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência",
[`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
};
const a = ano % 19, b = Math.floor(ano / 100), c = ano % 100;
const d = Math.floor(b / 4), e = b % 4, f_calc = Math.floor((b + 8) / 25);
const g = Math.floor((b - f_calc + 1) / 3), h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4), k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const mes = Math.floor((h + l - 7 * m + 114) / 31);
const dia = ((h + l - 7 * m + 114) % 31) + 1;
const pascoa = new Date(ano, mes - 1, dia);
const formatar = (dt: Date) => dt.toISOString().split('T')[0];
f[formatar(pascoa)] = "Páscoa";
f[formatar(new Date(pascoa.getTime() - 47 * 24 * 3600 * 1000))] = "Carnaval";
f[formatar(new Date(pascoa.getTime() - 2 * 24 * 3600 * 1000))] = "Sexta-feira Santa";
f[formatar(new Date(pascoa.getTime() + 60 * 24 * 3600 * 1000))] = "Corpo de Deus";
return f;
};
const AlunoHome = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const hojeStr = new Date().toISOString().split('T')[0];
const [selectedDate, setSelectedDate] = useState(hojeStr);
const [configEstagio, setConfigEstagio] = useState({ inicio: '2026-01-05', fim: '2026-05-30' });
const [presencas, setPresencas] = useState<Record<string, boolean>>({});
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
const [sumarios, setSumarios] = useState<Record<string, string>>({});
const [faltasJustificadas, setFaltasJustificadas] = useState<Record<string, any>>({});
const [pdf, setPdf] = useState<any>(null);
const [editandoSumario, setEditandoSumario] = useState(false);
useFocusEffect(
useCallback(() => {
const carregarTudo = async () => {
try {
const [config, pres, falt, sums, just] = await Promise.all([
AsyncStorage.getItem('@dados_estagio'),
AsyncStorage.getItem('@presencas'),
AsyncStorage.getItem('@faltas'),
AsyncStorage.getItem('@sumarios'),
AsyncStorage.getItem('@justificacoes')
]);
if (config) setConfigEstagio(JSON.parse(config));
if (pres) setPresencas(JSON.parse(pres));
if (falt) setFaltas(JSON.parse(falt));
if (sums) setSumarios(JSON.parse(sums));
if (just) setFaltasJustificadas(JSON.parse(just));
} catch (e) { console.error(e); }
};
carregarTudo();
}, [])
);
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
borda: isDarkMode ? '#333' : '#ddd',
}), [isDarkMode]);
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
const listaFeriados = useMemo(() => Object.keys(feriadosMap), [feriadosMap]);
const infoData = useMemo(() => {
const data = new Date(selectedDate);
const diaSemana = data.getDay();
const ehFimDeSemana = diaSemana === 0 || diaSemana === 6;
const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
const ehFuturo = selectedDate > hojeStr;
const nomeFeriado = feriadosMap[selectedDate];
return {
valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado,
podeMarcarPresenca: !ehFimDeSemana && !foraDoIntervalo && !ehFuturo && !nomeFeriado,
ehFuturo,
nomeFeriado
};
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
const diasMarcados: any = useMemo(() => {
const marcacoes: any = {};
listaFeriados.forEach(d => { marcacoes[d] = { marked: true, dotColor: '#0dcaf0' }; });
Object.keys(presencas).forEach((d) => {
const temSumario = sumarios[d] && sumarios[d].trim().length > 0;
marcacoes[d] = { marked: true, dotColor: temSumario ? '#198754' : '#ffc107' };
});
Object.keys(faltas).forEach((d) => {
marcacoes[d] = { marked: true, dotColor: faltasJustificadas[d] ? '#6c757d' : '#dc3545' };
});
marcacoes[selectedDate] = { ...(marcacoes[selectedDate] || {}), selected: true, selectedColor: '#0d6efd' };
return marcacoes;
}, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]);
const handlePresenca = async () => {
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "Data inválida.");
const novas = { ...presencas, [selectedDate]: true };
setPresencas(novas);
await AsyncStorage.setItem('@presencas', JSON.stringify(novas));
};
const handleFalta = async () => {
if (!infoData.valida) return Alert.alert("Bloqueado", "Data inválida.");
const novas = { ...faltas, [selectedDate]: true };
setFaltas(novas);
await AsyncStorage.setItem('@faltas', JSON.stringify(novas));
};
const guardarSumario = async () => {
await AsyncStorage.setItem('@sumarios', JSON.stringify(sumarios));
setEditandoSumario(false);
Alert.alert("Sucesso", "Sumário guardado!");
};
const escolherPDF = async () => {
const result = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
if (!result.canceled) setPdf(result.assets[0]);
};
const enviarJustificacao = async () => {
if (!pdf) return Alert.alert('Aviso', 'Anexe um PDF.');
const novas = { ...faltasJustificadas, [selectedDate]: pdf };
setFaltasJustificadas(novas);
await AsyncStorage.setItem('@justificacoes', JSON.stringify(novas));
setPdf(null);
};
const visualizarDocumento = async (uri: string) => {
if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(uri);
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.push('/perfil')}>
<Ionicons name="person-circle-outline" size={32} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
<TouchableOpacity onPress={() => router.push('/definicoes')}>
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
</TouchableOpacity>
</View>
<View style={styles.botoesLinha}>
<TouchableOpacity
style={[styles.btn, styles.btnPresenca, (!infoData.podeMarcarPresenca || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handlePresenca}
disabled={!infoData.podeMarcarPresenca || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
<Text style={styles.txtBtn}>Marcar Presença</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, styles.btnFalta, (!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handleFalta}
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
<Text style={styles.txtBtn}>{infoData.ehFuturo ? "Vou Faltar" : "Faltei"}</Text>
</TouchableOpacity>
</View>
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card }]}>
<Calendar
theme={{ calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto, todayTextColor: '#0d6efd', arrowColor: '#0d6efd' }}
markedDates={diasMarcados}
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}
/>
</View>
{infoData.nomeFeriado && (
<View style={styles.cardFeriado}>
<Ionicons name="gift-outline" size={18} color="#0dcaf0" />
<Text style={styles.txtFeriado}>
{selectedDate.endsWith('-06-24') ? "É Feriado em Vila do Conde: São João" : `Feriado: ${infoData.nomeFeriado}`}
</Text>
</View>
)}
{presencas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<View style={styles.rowTitle}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário do Dia</Text>
{!editandoSumario && (
<TouchableOpacity onPress={() => setEditandoSumario(true)}>
<Ionicons name="create-outline" size={22} color="#0d6efd" />
</TouchableOpacity>
)}
</View>
<TextInput
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto, opacity: editandoSumario ? 1 : 0.7 }]}
multiline editable={editandoSumario}
value={sumarios[selectedDate] || ''}
onChangeText={(txt) => setSumarios({ ...sumarios, [selectedDate]: txt })}
placeholder="Descreve as tuas tarefas..."
placeholderTextColor={themeStyles.textoSecundario}
/>
{editandoSumario && (
<TouchableOpacity style={styles.btnGuardar} onPress={guardarSumario}>
<Text style={styles.txtBtn}>Guardar Sumário</Text>
</TouchableOpacity>
)}
</View>
)}
{faltas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificação de Falta</Text>
{faltasJustificadas[selectedDate] ? (
<TouchableOpacity style={styles.btnVer} onPress={() => visualizarDocumento(faltasJustificadas[selectedDate].uri)}>
<Ionicons name="document-text-outline" size={20} color="#fff" />
<Text style={[styles.txtBtn, { marginLeft: 8 }]}>Ver Justificação (PDF)</Text>
</TouchableOpacity>
) : (
<View>
<TouchableOpacity style={[styles.btnAnexar, { borderColor: themeStyles.borda }]} onPress={escolherPDF}>
<Text style={{ color: themeStyles.texto }}>{pdf ? pdf.name : 'Anexar PDF'}</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btnGuardar, !pdf && styles.disabled]} onPress={enviarJustificacao} disabled={!pdf}>
<Text style={styles.txtBtn}>Enviar Justificação</Text>
</TouchableOpacity>
</View>
)}
</View>
)}
</ScrollView>
</SafeAreaView>
);
});
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
container: { padding: 20 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
title: { fontSize: 20, fontWeight: 'bold' },
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
btn: { padding: 15, borderRadius: 12, width: '48%', alignItems: 'center' },
btnPresenca: { backgroundColor: '#0d6efd' },
btnFalta: { backgroundColor: '#dc3545' },
btnGuardar: { backgroundColor: '#198754', padding: 12, borderRadius: 10, marginTop: 10, alignItems: 'center' },
btnAnexar: { borderWidth: 1, padding: 12, borderRadius: 10, marginBottom: 10, alignItems: 'center', borderStyle: 'dashed' },
btnVer: { backgroundColor: '#6c757d', padding: 12, borderRadius: 10, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
disabled: { opacity: 0.3 },
txtBtn: { color: '#fff', fontWeight: 'bold' },
cardCalendar: { borderRadius: 16, elevation: 4, padding: 10, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 },
cardFeriado: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 15, backgroundColor: 'rgba(13, 202, 240, 0.1)', padding: 10, borderRadius: 10 },
txtFeriado: { color: '#0dcaf0', fontWeight: 'bold', marginLeft: 8 },
card: { padding: 16, borderRadius: 16, marginTop: 20, elevation: 2 },
cardTitulo: { fontSize: 16, fontWeight: 'bold' },
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
input: { borderWidth: 1, borderRadius: 10, padding: 12, height: 100, textAlignVertical: 'top' }
});
export default AlunoHome;

25
app/_layout.tsx Normal file
View File

@@ -0,0 +1,25 @@
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { ThemeProvider, useTheme } from '../themecontext';
function RootLayoutContent() {
const { isDarkMode } = useTheme();
return (
<>
<StatusBar style={isDarkMode ? "light" : "dark"} />
<Stack screenOptions={{ headerShown: false }}>
{/* Removido o .tsx do name, o Expo Router usa apenas o nome do ficheiro */}
<Stack.Screen name="index" />
</Stack>
</>
);
}
export default function RootLayout() {
return (
<ThemeProvider>
<RootLayoutContent />
</ThemeProvider>
);
}

161
app/definicoes.tsx Normal file
View File

@@ -0,0 +1,161 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React, { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
import {
Alert,
Linking,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
} from 'react-native';
import { useTheme } from '../themecontext';
const Definicoes = memo(() => {
const router = useRouter();
const [notificacoes, setNotificacoes] = useState(true);
const { isDarkMode, toggleTheme } = useTheme();
// Otimização de cores para evitar lag no render
const cores = useMemo(() => ({
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#ffffff' : '#000000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
borda: isDarkMode ? '#333' : '#f1f3f5',
sair: '#dc3545',
azul: '#0d6efd'
}), [isDarkMode]);
const handleLogout = () => {
Alert.alert(
"Terminar Sessão",
"Tem a certeza que deseja sair da aplicação?",
[
{ text: "Cancelar", style: "cancel" },
{
text: "Sair",
style: "destructive",
onPress: () => router.replace('/')
}
]
);
};
const abrirEmail = () => Linking.openURL(`mailto:epvc@epvc.pt`);
const abrirEmail2 = () => Linking.openURL(`mailto:secretaria@epvc.pt`);
const abrirTelefone = () => Linking.openURL('tel:252 641 805');
return (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 40 }} />
</View>
<View style={styles.container}>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.subtituloSecao, { color: cores.azul }]}>Preferências</Text>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="notifications-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Notificações</Text>
</View>
<Switch
value={notificacoes}
onValueChange={setNotificacoes}
trackColor={{ false: '#767577', true: cores.azul }}
/>
</View>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name={isDarkMode ? "moon" : "sunny-outline"} size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Modo escuro</Text>
</View>
<Switch
value={isDarkMode}
onValueChange={toggleTheme} // Chamada direta otimizada pelo ThemeContext
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={isDarkMode ? '#fff' : '#f4f3f4'}
/>
</View>
<Text style={[styles.subtituloSecao, { color: cores.azul, marginTop: 25 }]}>Suporte e Contactos</Text>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Direção</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>epvc@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail2}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Secretaria</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirTelefone}>
<View style={styles.iconTexto}>
<Ionicons name="call-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Ligar para a Escola</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>252 641 805</Text>
</TouchableOpacity>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="information-circle-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Versão da app</Text>
</View>
<Text style={{ color: cores.textoSecundario }}>26.1.10</Text>
</View>
<TouchableOpacity
style={[styles.linha, { borderBottomWidth: 0 }]}
onPress={handleLogout}
>
<View style={styles.iconTexto}>
<Ionicons name="log-out-outline" size={22} color={cores.sair} style={{marginRight: 10}} />
<Text style={{ color: cores.sair, fontWeight: '600' }}>Terminar Sessão</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.textoSecundario} />
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
});
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 },
tituloGeral: { fontSize: 24, fontWeight: 'bold' },
subtituloSecao: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase', marginBottom: 5, marginLeft: 5 },
container: { padding: 20 },
card: { paddingHorizontal: 20, paddingVertical: 15, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
linha: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 15, borderBottomWidth: 1 },
iconTexto: { flexDirection: 'row', alignItems: 'center' }
});
export default Definicoes;

145
app/index.tsx Normal file
View File

@@ -0,0 +1,145 @@
// app/index.tsx - TELA DE LOGIN
import { Link, useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
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);
};
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>
</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>
</KeyboardAvoidingView>
);
}
// ESTILOS
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f8f9fa' },
content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
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' },
});

82
app/mainmenu.tsx Normal file
View File

@@ -0,0 +1,82 @@
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useTheme } from '../themecontext'; // Ajusta o caminho conforme a tua estrutura
const MainMenu = () => {
const { isDarkMode } = useTheme();
const themeStyles = {
fundo: isDarkMode ? '#121212' : '#f5f5f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#000',
textoSecundario: isDarkMode ? '#adb5bd' : 'gray',
borda: isDarkMode ? '#333' : '#e0e0e0',
iconFundo: isDarkMode ? '#2c2c2c' : '#f0f0f0'
};
const menuItems = [
{ id: 1, title: 'Criar Novo Sumário', icon: '📝' },
{ id: 2, title: 'Meus Sumários', icon: '📚' },
{ id: 3, title: 'Calendário', icon: '📅' },
{ id: 4, title: 'Notas', icon: '📊' },
{ id: 5, title: 'Horário', icon: '⏰' },
{ id: 6, title: 'Perfil', icon: '👤' },
{ id: 7, title: 'Configurações', icon: '⚙️' },
{ id: 8, title: 'Ajuda', icon: '❓' },
];
const handlePress = (title: string) => {
console.log(`Carregaste em: ${title}`);
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<View style={[styles.header, { backgroundColor: themeStyles.card, borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.greeting, { color: themeStyles.texto }]}>Bem-vindo de volta, Ricardo!</Text>
<Text style={[styles.subGreeting, { color: themeStyles.textoSecundario }]}>Desejamos-lhe um bom trabalho!</Text>
<Text style={styles.date}>06/01/2026, 14:41:52</Text>
</View>
<ScrollView contentContainerStyle={styles.menuGrid}>
{menuItems.map((item) => (
<TouchableOpacity
key={item.id}
style={[styles.menuItem, { backgroundColor: themeStyles.card }]}
onPress={() => handlePress(item.title)}
>
<View style={[styles.iconBox, { backgroundColor: themeStyles.iconFundo }]}>
<Text style={styles.icon}>{item.icon}</Text>
</View>
<Text style={[styles.menuText, { color: themeStyles.texto }]}>{item.title}</Text>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={styles.bigButton}
onPress={() => handlePress('Criar Novo Sumário')}
>
<Text style={styles.bigButtonText}>Criar Novo Sumário</Text>
</TouchableOpacity>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
header: { padding: 20, borderBottomWidth: 1 },
greeting: { fontSize: 22, fontWeight: 'bold' },
subGreeting: { fontSize: 16, marginTop: 5 },
date: { fontSize: 14, color: '#0d6efd', marginTop: 10, fontWeight: '500' },
menuGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', padding: 10 },
menuItem: { width: '48%', borderRadius: 10, padding: 15, marginBottom: 15, alignItems: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 3 },
iconBox: { width: 50, height: 50, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 10 },
icon: { fontSize: 24 },
menuText: { fontSize: 14, fontWeight: '600', textAlign: 'center' },
bigButton: { backgroundColor: '#0d6efd', margin: 20, padding: 18, borderRadius: 10, alignItems: 'center' },
bigButtonText: { color: 'white', fontSize: 18, fontWeight: 'bold' },
});
export default MainMenu;

232
app/perfil.tsx Normal file
View File

@@ -0,0 +1,232 @@
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet,
Text, TouchableOpacity, View
} from 'react-native';
import { useTheme } from '../themecontext';
export default function Perfil() {
const { isDarkMode } = useTheme();
const router = useRouter();
// Estados para dados dinâmicos e estatísticas
const [datas, setDatas] = useState({ inicio: '05/01/2026', fim: '30/05/2026' });
const [stats, setStats] = useState({
horasConcluidas: 0,
faltasTotais: 0,
faltasJustificadas: 0,
horasFaltam: 300
});
const themeStyles = {
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
borda: isDarkMode ? '#333' : '#f1f3f5',
};
useEffect(() => {
const carregarECalcular = async () => {
try {
const [config, presencasRaw, faltasRaw, justRaw] = await Promise.all([
AsyncStorage.getItem('@dados_estagio'),
AsyncStorage.getItem('@presencas'),
AsyncStorage.getItem('@faltas'),
AsyncStorage.getItem('@justificacoes')
]);
// 1. Carregar Configurações de Datas
if (config) {
const p = JSON.parse(config);
setDatas({
inicio: p.inicio || '05/01/2026',
fim: p.fim || '30/05/2026'
});
}
// 2. Calcular estatísticas baseadas nos objetos do calendário
const presencas = presencasRaw ? JSON.parse(presencasRaw) : {};
const faltas = faltasRaw ? JSON.parse(faltasRaw) : {};
const justificacoes = justRaw ? JSON.parse(justRaw) : {};
const totalDiasPresenca = Object.keys(presencas).length;
const totalFaltas = Object.keys(faltas).length;
const totalJustificadas = Object.keys(justificacoes).length;
const horasFeitas = totalDiasPresenca * 7; // 7h por dia
const totalHorasEstagio = 300;
setStats({
horasConcluidas: horasFeitas,
faltasTotais: totalFaltas,
faltasJustificadas: totalJustificadas,
horasFaltam: Math.max(0, totalHorasEstagio - horasFeitas)
});
} catch (e) {
console.error("Erro ao sincronizar dados do perfil", e);
}
};
carregarECalcular();
}, []);
return (
<SafeAreaView style={[styles.safe, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: themeStyles.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: themeStyles.texto }]}>Perfil do Aluno</Text>
<View style={styles.spacer} />
</View>
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
{/* Dados Pessoais - RESTAURADO TOTALMENTE */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Dados Pessoais</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Nome</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Ricardo Gomes</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Idade</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>17 anos</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Residência</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Junqueira, Vila do Conde</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>915783648</Text>
</View>
{/* NOVA SECÇÃO: Assiduidade Dinâmica */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Assiduidade</Text>
<View style={styles.rowStats}>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Faltas Totais</Text>
<Text style={[styles.valor, { color: '#dc3545', fontSize: 18 }]}>{stats.faltasTotais}</Text>
</View>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Justificadas</Text>
<Text style={[styles.valor, { color: '#198754', fontSize: 18 }]}>{stats.faltasJustificadas}</Text>
</View>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Ñ Justif.</Text>
<Text style={[styles.valor, { color: themeStyles.texto, fontSize: 18 }]}>
{stats.faltasTotais - stats.faltasJustificadas}
</Text>
</View>
</View>
</View>
{/* Empresa de Estágio - RESTAURADO TOTALMENTE */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Empresa de Estágio</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Curso</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Técnico de Informática</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Empresa</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Tech Solutions, Lda</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Morada</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Rua das papoilas, nº67</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Tutor</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Nicolau de Sousa</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>917892748</Text>
</View>
{/* Dados do Estágio - HORAS DINÂMICAS */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Dados do Estágio</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Início do Estágio</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.inicio}</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Fim do Estágio</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.fim}</Text>
<View style={[styles.estatisticasHoras, { borderBottomColor: themeStyles.borda }]}>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Totais</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>300h</Text>
</View>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Concluídas</Text>
<Text style={[styles.valor, {color: '#198754'}]}>{stats.horasConcluidas}h</Text>
</View>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Faltam</Text>
<Text style={[styles.valor, {color: '#dc3545'}]}>{stats.horasFaltam}h</Text>
</View>
</View>
<Text style={[styles.labelHorario, { color: themeStyles.texto }]}>Horário Semanal</Text>
<View style={[styles.tabela, { borderColor: themeStyles.borda }]}>
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#2c2c2c' : '#f8f9fa', borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Período</Text>
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Horário</Text>
</View>
<View style={[styles.linhaTab, { borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Manhã</Text>
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>09:30 - 13:00</Text>
</View>
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#252525' : '#fdfcfe', borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Almoço</Text>
<Text style={[styles.celulaValor, { fontWeight: '400', color: themeStyles.textoSecundario }]}>13:00 - 14:30</Text>
</View>
<View style={[styles.linhaTab, { borderBottomWidth: 0 }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Tarde</Text>
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>14:30 - 17:30</Text>
</View>
</View>
<Text style={styles.notaTotal}>Total: 7 horas diárias por presença</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 },
spacer: { width: 40 },
container: { padding: 20, gap: 20, paddingBottom: 40 },
tituloGeral: { fontSize: 22, fontWeight: 'bold' },
card: { padding: 20, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
tituloCard: { fontSize: 18, fontWeight: 'bold', color: '#0d6efd', textAlign: 'center', marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f3f5', paddingBottom: 8 },
label: { marginTop: 12, fontSize: 13 },
valor: { fontSize: 16, fontWeight: '600' },
labelHorario: { fontSize: 16, fontWeight: 'bold', marginTop: 20, marginBottom: 10, textAlign: 'center' },
tabela: { borderWidth: 1, borderRadius: 8, overflow: 'hidden' },
linhaTab: { flexDirection: 'row', borderBottomWidth: 1, paddingVertical: 8, alignItems: 'center' },
celulaHeader: { flex: 1, fontWeight: 'bold', textAlign: 'center', fontSize: 13 },
celulaLabel: { flex: 1, paddingLeft: 12, fontSize: 14 },
celulaValor: { flex: 1, textAlign: 'center', fontSize: 14, fontWeight: '600' },
notaTotal: { textAlign: 'center', fontSize: 12, color: '#0d6efd', marginTop: 8, fontWeight: '500' },
estatisticasHoras: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, paddingBottom: 15, marginTop: 5 },
rowStats: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 },
itemStat: { alignItems: 'center', flex: 1 }
});

312
app/register.tsx Normal file
View File

@@ -0,0 +1,312 @@
// app/register.tsx - TELA DE CRIAR CONTA (CORRIGIDA)
import React, { useState } from 'react';
import {
View,
TextInput,
TouchableOpacity,
Text,
StyleSheet,
ScrollView,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
Platform
} from 'react-native';
import { Link } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
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',
},
});