Versão 26.01.10 - Atualizações
43
.gitignore
vendored
Normal 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
@@ -0,0 +1 @@
|
||||
{ "recommendations": ["expo.vscode-expo-tools"] }
|
||||
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
}
|
||||
}
|
||||
50
README.md
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
},
|
||||
});
|
||||
BIN
assets/images/android-icon-background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/android-icon-foreground.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
assets/images/android-icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
assets/images/partial-react-logo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/images/react-logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/images/react-logo@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/react-logo@3x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/images/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
25
components/external-link.tsx
Normal 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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
79
components/parallax-scroll-view.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
60
components/themed-text.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
14
components/themed-view.tsx
Normal 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} />;
|
||||
}
|
||||
45
components/ui/collapsible.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
32
components/ui/icon-symbol.ios.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
41
components/ui/icon-symbol.tsx
Normal 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
@@ -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
@@ -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
@@ -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);
|
||||
1
hooks/use-color-scheme.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
21
hooks/use-color-scheme.web.ts
Normal 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
@@ -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
52
package.json
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
}
|
||||