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

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
app-example
# generated native folders
/ios
/android

1
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }

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

@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}

50
README.md Normal file
View File

@@ -0,0 +1,50 @@
# Welcome to your Expo app 👋
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

48
app.json Normal file
View File

@@ -0,0 +1,48 @@
{
"expo": {
"name": "estagios_pap",
"slug": "estagios_pap",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "estagiospap",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}

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',
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,25 @@
import { Href, Link } from 'expo-router';
import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
import { type ComponentProps } from 'react';
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
export function ExternalLink({ href, ...rest }: Props) {
return (
<Link
target="_blank"
{...rest}
href={href}
onPress={async (event) => {
if (process.env.EXPO_OS !== 'web') {
// Prevent the default behavior of linking to the default browser on native.
event.preventDefault();
// Open the link in an in-app browser.
await openBrowserAsync(href, {
presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
});
}
}}
/>
);
}

18
components/haptic-tab.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
import { PlatformPressable } from '@react-navigation/elements';
import * as Haptics from 'expo-haptics';
export function HapticTab(props: BottomTabBarButtonProps) {
return (
<PlatformPressable
{...props}
onPressIn={(ev) => {
if (process.env.EXPO_OS === 'ios') {
// Add a soft haptic feedback when pressing down on the tabs.
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
props.onPressIn?.(ev);
}}
/>
);
}

19
components/hello-wave.tsx Normal file
View File

@@ -0,0 +1,19 @@
import Animated from 'react-native-reanimated';
export function HelloWave() {
return (
<Animated.Text
style={{
fontSize: 28,
lineHeight: 32,
marginTop: -6,
animationName: {
'50%': { transform: [{ rotate: '25deg' }] },
},
animationIterationCount: 4,
animationDuration: '300ms',
}}>
👋
</Animated.Text>
);
}

View File

@@ -0,0 +1,79 @@
import type { PropsWithChildren, ReactElement } from 'react';
import { StyleSheet } from 'react-native';
import Animated, {
interpolate,
useAnimatedRef,
useAnimatedStyle,
useScrollOffset,
} from 'react-native-reanimated';
import { ThemedView } from '@/components/themed-view';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useThemeColor } from '@/hooks/use-theme-color';
const HEADER_HEIGHT = 250;
type Props = PropsWithChildren<{
headerImage: ReactElement;
headerBackgroundColor: { dark: string; light: string };
}>;
export default function ParallaxScrollView({
children,
headerImage,
headerBackgroundColor,
}: Props) {
const backgroundColor = useThemeColor({}, 'background');
const colorScheme = useColorScheme() ?? 'light';
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(scrollRef);
const headerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(
scrollOffset.value,
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
),
},
{
scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
},
],
};
});
return (
<Animated.ScrollView
ref={scrollRef}
style={{ backgroundColor, flex: 1 }}
scrollEventThrottle={16}>
<Animated.View
style={[
styles.header,
{ backgroundColor: headerBackgroundColor[colorScheme] },
headerAnimatedStyle,
]}>
{headerImage}
</Animated.View>
<ThemedView style={styles.content}>{children}</ThemedView>
</Animated.ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HEADER_HEIGHT,
overflow: 'hidden',
},
content: {
flex: 1,
padding: 32,
gap: 16,
overflow: 'hidden',
},
});

View File

@@ -0,0 +1,60 @@
import { StyleSheet, Text, type TextProps } from 'react-native';
import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedTextProps = TextProps & {
lightColor?: string;
darkColor?: string;
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
};
export function ThemedText({
style,
lightColor,
darkColor,
type = 'default',
...rest
}: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
return (
<Text
style={[
{ color },
type === 'default' ? styles.default : undefined,
type === 'title' ? styles.title : undefined,
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
type === 'subtitle' ? styles.subtitle : undefined,
type === 'link' ? styles.link : undefined,
style,
]}
{...rest}
/>
);
}
const styles = StyleSheet.create({
default: {
fontSize: 16,
lineHeight: 24,
},
defaultSemiBold: {
fontSize: 16,
lineHeight: 24,
fontWeight: '600',
},
title: {
fontSize: 32,
fontWeight: 'bold',
lineHeight: 32,
},
subtitle: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
lineHeight: 30,
fontSize: 16,
color: '#0a7ea4',
},
});

View File

@@ -0,0 +1,14 @@
import { View, type ViewProps } from 'react-native';
import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedViewProps = ViewProps & {
lightColor?: string;
darkColor?: string;
};
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
}

View File

@@ -0,0 +1,45 @@
import { PropsWithChildren, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
const [isOpen, setIsOpen] = useState(false);
const theme = useColorScheme() ?? 'light';
return (
<ThemedView>
<TouchableOpacity
style={styles.heading}
onPress={() => setIsOpen((value) => !value)}
activeOpacity={0.8}>
<IconSymbol
name="chevron.right"
size={18}
weight="medium"
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
/>
<ThemedText type="defaultSemiBold">{title}</ThemedText>
</TouchableOpacity>
{isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
</ThemedView>
);
}
const styles = StyleSheet.create({
heading: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
content: {
marginTop: 6,
marginLeft: 24,
},
});

View File

@@ -0,0 +1,32 @@
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
import { StyleProp, ViewStyle } from 'react-native';
export function IconSymbol({
name,
size = 24,
color,
style,
weight = 'regular',
}: {
name: SymbolViewProps['name'];
size?: number;
color: string;
style?: StyleProp<ViewStyle>;
weight?: SymbolWeight;
}) {
return (
<SymbolView
weight={weight}
tintColor={color}
resizeMode="scaleAspectFit"
name={name}
style={[
{
width: size,
height: size,
},
style,
]}
/>
);
}

View File

@@ -0,0 +1,41 @@
// Fallback for using MaterialIcons on Android and web.
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
import { ComponentProps } from 'react';
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
type IconSymbolName = keyof typeof MAPPING;
/**
* Add your SF Symbols to Material Icons mappings here.
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
*/
const MAPPING = {
'house.fill': 'home',
'paperplane.fill': 'send',
'chevron.left.forwardslash.chevron.right': 'code',
'chevron.right': 'chevron-right',
} as IconMapping;
/**
* An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
* This ensures a consistent look across platforms, and optimal resource usage.
* Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
*/
export function IconSymbol({
name,
size = 24,
color,
style,
}: {
name: IconSymbolName;
size?: number;
color: string | OpaqueColorValue;
style?: StyleProp<TextStyle>;
weight?: SymbolWeight;
}) {
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
}

53
constants/theme.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
*/
import { Platform } from 'react-native';
const tintColorLight = '#0a7ea4';
const tintColorDark = '#fff';
export const Colors = {
light: {
text: '#11181C',
background: '#fff',
tint: tintColorLight,
icon: '#687076',
tabIconDefault: '#687076',
tabIconSelected: tintColorLight,
},
dark: {
text: '#ECEDEE',
background: '#151718',
tint: tintColorDark,
icon: '#9BA1A6',
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},
};
export const Fonts = Platform.select({
ios: {
/** iOS `UIFontDescriptorSystemDesignDefault` */
sans: 'system-ui',
/** iOS `UIFontDescriptorSystemDesignSerif` */
serif: 'ui-serif',
/** iOS `UIFontDescriptorSystemDesignRounded` */
rounded: 'ui-rounded',
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
mono: 'ui-monospace',
},
default: {
sans: 'normal',
serif: 'serif',
rounded: 'normal',
mono: 'monospace',
},
web: {
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Georgia, 'Times New Roman', serif",
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
},
});

10
eslint.config.js Normal file
View File

@@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
},
]);

25
firebase.js Normal file
View File

@@ -0,0 +1,25 @@
// Import the functions you need from the SDKs you need
import { getAnalytics } from "firebase/analytics";
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "AIzaSyDwRy3i-iDMp-EMaD951kCHA_sYchUn3XQ",
authDomain: "estagios-f24a0.firebaseapp.com",
projectId: "estagios-f24a0",
storageBucket: "estagios-f24a0.firebasestorage.app",
messagingSenderId: "74100452508",
appId: "1:74100452508:web:17e7e082de613de00ad159",
measurementId: "G-114DX1B2SR"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
export const auth = getAuth(app);
export const db = getFirestore(app);

View File

@@ -0,0 +1 @@
export { useColorScheme } from 'react-native';

View File

@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';
/**
* To support static rendering, this value needs to be re-calculated on the client side for web
*/
export function useColorScheme() {
const [hasHydrated, setHasHydrated] = useState(false);
useEffect(() => {
setHasHydrated(true);
}, []);
const colorScheme = useRNColorScheme();
if (hasHydrated) {
return colorScheme;
}
return 'light';
}

21
hooks/use-theme-color.ts Normal file
View File

@@ -0,0 +1,21 @@
/**
* Learn more about light and dark modes:
* https://docs.expo.dev/guides/color-schemes/
*/
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
) {
const theme = useColorScheme() ?? 'light';
const colorFromProps = props[theme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
}
}

13711
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "estagios_pap",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"lint": "expo lint"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.27",
"expo-constants": "~18.0.11",
"expo-document-picker": "~14.0.8",
"expo-font": "~14.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.10",
"expo-router": "~6.0.17",
"expo-sharing": "~14.0.8",
"expo-splash-screen": "~31.0.12",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10",
"firebase": "^12.7.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-calendars": "^1.1313.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"private": true
}

112
scripts/reset-project.js Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* This script is used to reset the project to a blank state.
* It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const root = process.cwd();
const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
const exampleDir = "app-example";
const newAppDir = "app";
const exampleDirPath = path.join(root, exampleDir);
const indexContent = `import { Text, View } from "react-native";
export default function Index() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/index.tsx to edit this screen.</Text>
</View>
);
}
`;
const layoutContent = `import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
`;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const moveDirectories = async (userInput) => {
try {
if (userInput === "y") {
// Create the app-example directory
await fs.promises.mkdir(exampleDirPath, { recursive: true });
console.log(`📁 /${exampleDir} directory created.`);
}
// Move old directories to new app-example directory or delete them
for (const dir of oldDirs) {
const oldDirPath = path.join(root, dir);
if (fs.existsSync(oldDirPath)) {
if (userInput === "y") {
const newDirPath = path.join(root, exampleDir, dir);
await fs.promises.rename(oldDirPath, newDirPath);
console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
} else {
await fs.promises.rm(oldDirPath, { recursive: true, force: true });
console.log(`❌ /${dir} deleted.`);
}
} else {
console.log(`➡️ /${dir} does not exist, skipping.`);
}
}
// Create new /app directory
const newAppDirPath = path.join(root, newAppDir);
await fs.promises.mkdir(newAppDirPath, { recursive: true });
console.log("\n📁 New /app directory created.");
// Create index.tsx
const indexPath = path.join(newAppDirPath, "index.tsx");
await fs.promises.writeFile(indexPath, indexContent);
console.log("📄 app/index.tsx created.");
// Create _layout.tsx
const layoutPath = path.join(newAppDirPath, "_layout.tsx");
await fs.promises.writeFile(layoutPath, layoutContent);
console.log("📄 app/_layout.tsx created.");
console.log("\n✅ Project reset complete. Next steps:");
console.log(
`1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
userInput === "y"
? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
: ""
}`
);
} catch (error) {
console.error(`❌ Error during script execution: ${error.message}`);
}
};
rl.question(
"Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
(answer) => {
const userInput = answer.trim().toLowerCase() || "y";
if (userInput === "y" || userInput === "n") {
moveDirectories(userInput).finally(() => rl.close());
} else {
console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
rl.close();
}
}
);

28
themecontext.tsx Normal file
View File

@@ -0,0 +1,28 @@
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext({
isDarkMode: false,
toggleTheme: () => {},
});
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
// useCallback garante que a função não mude nunca
const toggleTheme = useCallback(() => {
setIsDarkMode(prev => !prev);
}, []);
const value = useMemo(() => ({
isDarkMode,
toggleTheme
}), [isDarkMode, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
}