This commit is contained in:
2026-01-20 17:15:23 +00:00
parent d443933bdc
commit 632aed8181
8 changed files with 429 additions and 445 deletions

View File

@@ -1,145 +1,36 @@
// app/index.tsx - TELA DE LOGIN
import { Link, useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
// app/index.tsx
import { useRouter } from 'expo-router';
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
import Auth from '../components/Auth';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
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);
const handleLoginSuccess = () => {
router.replace('/AlunoHome');
};
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>
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: '#f8f9fa' }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>📱 Estágios+</Text>
<Text style={styles.subtitle}>Escola Profissional de Vila do Conde</Text>
</View>
{/* Componente Auth */}
<Auth onLoginSuccess={handleLoginSuccess} />
</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>
</ScrollView>
</KeyboardAvoidingView>
);
}
// ESTILOS
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f8f9fa' },
content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
scrollContainer: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 40 },
content: { flex: 1, justifyContent: 'center' },
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' },
});
});

32
app/lib/supabase.ts Normal file
View File

@@ -0,0 +1,32 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient, processLock } from '@supabase/supabase-js'
import { AppState, Platform } from 'react-native'
import 'react-native-url-polyfill/auto'
const supabaseUrl = 'https://ssorfpctjeujolmtkfib.supabase.co'
const supabaseAnonKey = 'sb_publishable_SDocGprdYkUKi04FyfVqmA_Ykirp9cK'
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
...(Platform.OS !== "web" ? { storage: AsyncStorage } : {}),
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
lock: processLock,
},
})
// Tells Supabase Auth to continuously refresh the session automatically
// if the app is in the foreground. When this is added, you will continue
// to receive `onAuthStateChange` events with the `TOKEN_REFRESHED` or
// `SIGNED_OUT` event if the user's session is terminated. This should
// only be registered once.
if (Platform.OS !== "web") {
AppState.addEventListener('change', (state) => {
if (state === 'active') {
supabase.auth.startAutoRefresh()
} else {
supabase.auth.stopAutoRefresh()
}
})
}

131
app/redefenirsenha.tsx Normal file
View File

@@ -0,0 +1,131 @@
// app/forgot-password.tsx
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../app/lib/supabase';
export default function ForgotPassword() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSendResetEmail = async () => {
if (!email) {
Alert.alert('Atenção', 'Insira seu email');
return;
}
setLoading(true);
try {
const { error } = await supabase.auth.resetPasswordForEmail(email);
if (error) throw error;
Alert.alert('Sucesso!', 'Verifique seu email para redefinir a palavra-passe');
router.back(); // volta para login
} catch (err: any) {
Alert.alert('Erro', err.message);
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: '#f8f9fa' }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<View style={styles.container}>
<Text style={styles.title}>Recuperar Palavra-passe</Text>
<Text style={styles.subtitle}>Insira seu email para receber o link de redefinição</Text>
{/* INPUT EMAIL */}
<TextInput
style={styles.input}
placeholder="email@address.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
{/* BOTÃO ENVIAR LINK */}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendResetEmail}
disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>ENVIAR LINK</Text>}
</TouchableOpacity>
{/* BOTÃO VOLTAR */}
<TouchableOpacity onPress={() => router.push('/')} style={styles.backContainer}>
<Text style={styles.backText}> Voltar para Login</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
scrollContainer: { flexGrow: 1, justifyContent: 'center', padding: 24 },
container: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
logo: {
width: 120,
height: 120,
alignSelf: 'center',
marginBottom: 20,
},
title: { fontSize: 24, fontWeight: '700', color: '#2d3436', marginBottom: 8, textAlign: 'center' },
subtitle: { fontSize: 14, color: '#636e72', marginBottom: 20, textAlign: 'center' },
input: {
backgroundColor: '#f1f2f6',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
marginBottom: 20,
borderWidth: 0,
color: '#2d3436',
},
button: {
backgroundColor: '#0984e3',
borderRadius: 12,
paddingVertical: 16,
alignItems: 'center',
marginBottom: 12,
shadowColor: '#0984e3',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 3,
},
buttonDisabled: { backgroundColor: '#74b9ff' },
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
backContainer: { marginTop: 8, alignItems: 'center' },
backText: { color: '#0984e3', fontSize: 15, fontWeight: '500' },
});

View File

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

0
assets/images/logo.png Normal file
View File

101
components/Auth.tsx Normal file
View File

@@ -0,0 +1,101 @@
import { useRouter } from 'expo-router'; // IMPORTAR ROUTER
import { useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { supabase } from '../app/lib/supabase';
interface AuthProps {
onLoginSuccess?: () => void;
}
export default function Auth({ onLoginSuccess }: AuthProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter(); // INICIALIZA O ROUTER
// LOGIN
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Atenção', 'Preencha todos os campos');
return;
}
setLoading(true);
try {
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) throw error;
Alert.alert('Bem-vindo(a)!');
if (onLoginSuccess) onLoginSuccess();
} catch (err: any) {
Alert.alert('Erro', err.message);
} finally {
setLoading(false);
}
};
return (
<View style={styles.form}>
{/* EMAIL */}
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder="email@address.com"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
editable={!loading}
/>
{/* PASSWORD */}
<Text style={styles.label}>Palavra-passe</Text>
<TextInput
style={styles.input}
placeholder="Insira a sua palavra-passe"
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!loading}
/>
{/* BOTÃO ENTRAR MODERNO */}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>ENTRAR</Text>
)}
</TouchableOpacity>
{/* TEXTO DE ESQUECI A SENHA → NAVEGA */}
<TouchableOpacity onPress={() => router.push('/redefenirsenha')}>
<Text style={styles.forgotText}>Esqueceu a palavra-passe?</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
form: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
marginTop: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8 },
input: { backgroundColor: '#f1f2f6', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, marginBottom: 20, borderWidth: 0, color: '#2d3436' },
button: { backgroundColor: '#0984e3', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginBottom: 12, shadowColor: '#0984e3', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 3 },
buttonDisabled: { backgroundColor: '#74b9ff' },
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500', textAlign: 'center', marginTop: 8 },
});

145
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@supabase/supabase-js": "^2.91.0",
"expo": "~54.0.27",
"expo-constants": "~18.0.11",
"expo-document-picker": "~14.0.8",
@@ -35,6 +36,7 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
},
@@ -3463,6 +3465,107 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@supabase/auth-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.91.0.tgz",
"integrity": "sha512-9ywvsKLsxTwv7fvN5fXzP3UfRreqrX2waylTBDu0lkmeHXa8WtSQS9e0WV9FBduiazYqQbgfBQXBNPRPsRgWOQ==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.91.0.tgz",
"integrity": "sha512-WaakXOqLK1mLtBNFXp5o5T+LlI6KZuADSeXz+9ofPRG5OpVSvW148LVJB1DRZ16Phck1a0YqIUswOUgxCz6vMw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.91.0.tgz",
"integrity": "sha512-5S41zv2euNpGucvtM4Wy+xOmLznqt/XO+Lh823LOFEQ00ov7QJfvqb6VzIxufvzhooZpmGR0BxvMcJtWxCIFdQ==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.91.0.tgz",
"integrity": "sha512-u2YuJFG35umw8DO9beC27L/jYXm3KhF+73WQwbynMpV0tXsFIA0DOGRM0NgRyy03hJIdO6mxTTwe8efW3yx3Tg==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js/node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@supabase/storage-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.91.0.tgz",
"integrity": "sha512-CI7fsVIBQHfNObqU9kmyQ1GWr+Ug44y4rSpvxT4LdQB9tlhg1NTBov6z7Dlmt8d6lGi/8a9lf/epCDxyWI792g==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.91.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.91.0.tgz",
"integrity": "sha512-Rjb0QqkKrmXMVwUOdEqysPBZ0ZDZakeptTkUa6k2d8r3strBdbWVDqjOdkCjAmvvZMtXecBeyTyMEXD1Zzjfvg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.91.0",
"@supabase/functions-js": "2.91.0",
"@supabase/postgrest-js": "2.91.0",
"@supabase/realtime-js": "2.91.0",
"@supabase/storage-js": "2.91.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3584,6 +3687,12 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
@@ -3600,6 +3709,15 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -7709,6 +7827,15 @@
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -10703,6 +10830,18 @@
"integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
"license": "MIT"
},
"node_modules/react-native-url-polyfill": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
"integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==",
"license": "MIT",
"dependencies": {
"whatwg-url-without-unicode": "8.0.0-3"
},
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/react-native-web": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -11994,9 +12133,9 @@
}
},
"node_modules/tar": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.4.tgz",
"integrity": "sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",

View File

@@ -16,6 +16,7 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@supabase/supabase-js": "^2.91.0",
"expo": "~54.0.27",
"expo-constants": "~18.0.11",
"expo-document-picker": "~14.0.8",
@@ -38,6 +39,7 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
},