feat: persistencia de sesion, logout y validacion de campos
This commit is contained in:
@@ -38,7 +38,7 @@ PostgreSQL — Usuario → Domicilio → Zona → Ruta
|
|||||||
- [x] 3 eventos de notificación: ROUTE_START, TRUCK_PROXIMITY, ROUTE_COMPLETED
|
- [x] 3 eventos de notificación: ROUTE_START, TRUCK_PROXIMITY, ROUTE_COMPLETED
|
||||||
- [x] Guía de separación de residuos (funciona offline)
|
- [x] Guía de separación de residuos (funciona offline)
|
||||||
- [x] RBAC — cada usuario solo consulta su ruta
|
- [x] RBAC — cada usuario solo consulta su ruta
|
||||||
- [ ] Buzón de reportes (en desarrollo)
|
- [X] Buzón de reportes (en desarrollo)
|
||||||
- [ ] Notificaciones push FCM (en desarrollo)
|
- [ ] Notificaciones push FCM (en desarrollo)
|
||||||
|
|
||||||
## 🛠️ Stack Tecnológico
|
## 🛠️ Stack Tecnológico
|
||||||
|
|||||||
156
frontend/App.js
156
frontend/App.js
@@ -1,11 +1,12 @@
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native';
|
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
const API_URL = 'http://10.137.112.65:8000';
|
const API_URL = 'http://10.137.112.65:8000';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [screen, setScreen] = useState('login');
|
const [screen, setScreen] = useState('splash');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [token, setToken] = useState(null);
|
const [token, setToken] = useState(null);
|
||||||
@@ -15,7 +16,56 @@ export default function App() {
|
|||||||
const [colonia, setColonia] = useState('');
|
const [colonia, setColonia] = useState('');
|
||||||
const [direccion, setDireccion] = useState('');
|
const [direccion, setDireccion] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cargarSesion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cargarSesion = async () => {
|
||||||
|
try {
|
||||||
|
const t = await AsyncStorage.getItem('token');
|
||||||
|
const d = await AsyncStorage.getItem('domicilioId');
|
||||||
|
if (t && d) {
|
||||||
|
setToken(t);
|
||||||
|
setDomicilioId(parseInt(d));
|
||||||
|
setScreen('eta');
|
||||||
|
consultarETA(parseInt(d), t);
|
||||||
|
} else if (t) {
|
||||||
|
setToken(t);
|
||||||
|
setScreen('domicilio');
|
||||||
|
} else {
|
||||||
|
setScreen('login');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setScreen('login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const guardarSesion = async (t, dId) => {
|
||||||
|
await AsyncStorage.setItem('token', t);
|
||||||
|
if (dId) await AsyncStorage.setItem('domicilioId', String(dId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cerrarSesion = async () => {
|
||||||
|
await AsyncStorage.clear();
|
||||||
|
setToken(null);
|
||||||
|
setDomicilioId(null);
|
||||||
|
setEta(null);
|
||||||
|
setEmail('');
|
||||||
|
setPassword('');
|
||||||
|
setDireccion('');
|
||||||
|
setColonia('');
|
||||||
|
setScreen('login');
|
||||||
|
};
|
||||||
|
|
||||||
const register = async () => {
|
const register = async () => {
|
||||||
|
if (!email.trim() || !password.trim()) {
|
||||||
|
Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 4) {
|
||||||
|
Alert.alert('Contraseña débil', 'La contraseña debe tener al menos 4 caracteres');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/auth/register`, {
|
const res = await fetch(`${API_URL}/auth/register`, {
|
||||||
@@ -26,17 +76,22 @@ export default function App() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.access_token) {
|
if (data.access_token) {
|
||||||
setToken(data.access_token);
|
setToken(data.access_token);
|
||||||
|
await guardarSesion(data.access_token, null);
|
||||||
setScreen('domicilio');
|
setScreen('domicilio');
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Error', data.detail || 'Error al registrar');
|
Alert.alert('Error', data.detail || 'Error al registrar');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
Alert.alert('Error', 'No se pudo conectar al servidor');
|
Alert.alert('Error', 'No se pudo conectar al servidor');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
if (!email.trim() || !password.trim()) {
|
||||||
|
Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/auth/login`, {
|
const res = await fetch(`${API_URL}/auth/login`, {
|
||||||
@@ -47,11 +102,19 @@ export default function App() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.access_token) {
|
if (data.access_token) {
|
||||||
setToken(data.access_token);
|
setToken(data.access_token);
|
||||||
|
await guardarSesion(data.access_token, null);
|
||||||
|
const domGuardado = await AsyncStorage.getItem('domicilioId');
|
||||||
|
if (domGuardado) {
|
||||||
|
setDomicilioId(parseInt(domGuardado));
|
||||||
|
setScreen('eta');
|
||||||
|
consultarETA(parseInt(domGuardado), data.access_token);
|
||||||
|
} else {
|
||||||
setScreen('domicilio');
|
setScreen('domicilio');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Error', 'Credenciales incorrectas');
|
Alert.alert('Error', 'Credenciales incorrectas');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
Alert.alert('Error', 'No se pudo conectar al servidor');
|
Alert.alert('Error', 'No se pudo conectar al servidor');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -66,56 +129,64 @@ export default function App() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ direccion, colonia, lat: 20.5185, lng: -100.8450 }),
|
||||||
direccion,
|
|
||||||
colonia,
|
|
||||||
lat: 20.5185,
|
|
||||||
lng: -100.8450,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
setDomicilioId(data.id);
|
setDomicilioId(data.id);
|
||||||
|
await guardarSesion(token, data.id);
|
||||||
setScreen('eta');
|
setScreen('eta');
|
||||||
consultarETA(data.id);
|
consultarETA(data.id, token);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Error', data.detail || 'Colonia no encontrada');
|
Alert.alert('Error', data.detail || 'Colonia no encontrada');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
Alert.alert('Error', 'No se pudo guardar el domicilio');
|
Alert.alert('Error', 'No se pudo guardar el domicilio');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const consultarETA = async (id) => {
|
const consultarETA = async (id, t) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/eta/${id}`, {
|
const res = await fetch(`${API_URL}/eta/${id || domicilioId}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${t || token}` },
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setEta(data);
|
if (data.mensaje) setEta(data);
|
||||||
} catch (e) {
|
else {
|
||||||
|
await cerrarSesion();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
Alert.alert('Error', 'No se pudo obtener el ETA');
|
Alert.alert('Error', 'No se pudo obtener el ETA');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (screen === 'splash') return (
|
||||||
|
<View style={styles.splashContainer}>
|
||||||
|
<Text style={styles.splashEmoji}>🚛</Text>
|
||||||
|
<Text style={styles.splashTitle}>BasuraApp</Text>
|
||||||
|
<ActivityIndicator color="#fff" style={{ marginTop: 24 }} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
if (screen === 'login') return (
|
if (screen === 'login') return (
|
||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<Text style={styles.title}>🚛 BasuraApp</Text>
|
<Text style={styles.bigEmoji}>🚛</Text>
|
||||||
|
<Text style={styles.title}>BasuraApp</Text>
|
||||||
<Text style={styles.subtitle}>Ingresa a tu cuenta</Text>
|
<Text style={styles.subtitle}>Ingresa a tu cuenta</Text>
|
||||||
<TextInput style={styles.input} placeholder="Email" value={email}
|
<TextInput style={styles.input} placeholder="Email" value={email}
|
||||||
onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
|
onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
|
||||||
<TextInput style={styles.input} placeholder="Contraseña" value={password}
|
<TextInput style={styles.input} placeholder="Contraseña" value={password}
|
||||||
onChangeText={setPassword} secureTextEntry />
|
onChangeText={setPassword} secureTextEntry />
|
||||||
{loading ? <ActivityIndicator color="#1a7a4a" /> : <>
|
{loading ? <ActivityIndicator color="#1a7a4a" size="large" style={{ marginTop: 16 }} /> : <>
|
||||||
<TouchableOpacity style={styles.btn} onPress={login}>
|
<TouchableOpacity style={styles.btn} onPress={login}>
|
||||||
<Text style={styles.btnText}>Iniciar sesión</Text>
|
<Text style={styles.btnText}>Iniciar sesión</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.btnSecondary} onPress={register}>
|
<TouchableOpacity style={styles.btnSecondary} onPress={register}>
|
||||||
<Text style={styles.btnSecondaryText}>Registrarme</Text>
|
<Text style={styles.btnSecondaryText}>Crear cuenta nueva</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>}
|
</>}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -129,11 +200,14 @@ export default function App() {
|
|||||||
value={direccion} onChangeText={setDireccion} />
|
value={direccion} onChangeText={setDireccion} />
|
||||||
<TextInput style={styles.input} placeholder="Colonia (ej: Zona Centro)"
|
<TextInput style={styles.input} placeholder="Colonia (ej: Zona Centro)"
|
||||||
value={colonia} onChangeText={setColonia} />
|
value={colonia} onChangeText={setColonia} />
|
||||||
<Text style={styles.hint}>Colonias disponibles: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes</Text>
|
<Text style={styles.hint}>Colonias: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes</Text>
|
||||||
{loading ? <ActivityIndicator color="#1a7a4a" /> :
|
{loading ? <ActivityIndicator color="#1a7a4a" size="large" /> :
|
||||||
<TouchableOpacity style={styles.btn} onPress={guardarDomicilio}>
|
<TouchableOpacity style={styles.btn} onPress={guardarDomicilio}>
|
||||||
<Text style={styles.btnText}>Guardar y ver horario</Text>
|
<Text style={styles.btnText}>Guardar y ver horario</Text>
|
||||||
</TouchableOpacity>}
|
</TouchableOpacity>}
|
||||||
|
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 24 }}>
|
||||||
|
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,9 +216,11 @@ export default function App() {
|
|||||||
<Text style={styles.title}>🕐 Horario de recolección</Text>
|
<Text style={styles.title}>🕐 Horario de recolección</Text>
|
||||||
{loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <>
|
{loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <>
|
||||||
<View style={styles.etaCard}>
|
<View style={styles.etaCard}>
|
||||||
<Text style={styles.etaEvento}>{eta.evento === 'TRUCK_PROXIMITY' ? '🚨 ¡Camión cercano!' :
|
<Text style={styles.etaEvento}>
|
||||||
|
{eta.evento === 'TRUCK_PROXIMITY' ? '🚨 ¡Camión cercano!' :
|
||||||
eta.evento === 'ROUTE_START' ? '🟢 Ruta iniciada' :
|
eta.evento === 'ROUTE_START' ? '🟢 Ruta iniciada' :
|
||||||
eta.evento === 'ROUTE_COMPLETED' ? '✅ Servicio finalizado' : '🚛 En camino'}</Text>
|
eta.evento === 'ROUTE_COMPLETED' ? '✅ Servicio finalizado' : '🚛 En camino'}
|
||||||
|
</Text>
|
||||||
<Text style={styles.etaMensaje}>{eta.mensaje}</Text>
|
<Text style={styles.etaMensaje}>{eta.mensaje}</Text>
|
||||||
<View style={styles.ventanaBox}>
|
<View style={styles.ventanaBox}>
|
||||||
<Text style={styles.ventanaLabel}>Ventana de llegada</Text>
|
<Text style={styles.ventanaLabel}>Ventana de llegada</Text>
|
||||||
@@ -155,7 +231,7 @@ export default function App() {
|
|||||||
<View style={styles.privacyBox}>
|
<View style={styles.privacyBox}>
|
||||||
<Text style={styles.privacyText}>🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión.</Text>
|
<Text style={styles.privacyText}>🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión.</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.btn} onPress={() => consultarETA(domicilioId)}>
|
<TouchableOpacity style={styles.btn} onPress={() => consultarETA(domicilioId, token)}>
|
||||||
<Text style={styles.btnText}>Actualizar</Text>
|
<Text style={styles.btnText}>Actualizar</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('separacion')}>
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('separacion')}>
|
||||||
@@ -164,6 +240,9 @@ export default function App() {
|
|||||||
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('reporte')}>
|
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('reporte')}>
|
||||||
<Text style={styles.btnSecondaryText}>📋 Reportar incidencia</Text>
|
<Text style={styles.btnSecondaryText}>📋 Reportar incidencia</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 16 }}>
|
||||||
|
<Text style={styles.logoutText}>Cerrar sesión</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</> : <Text>Sin datos</Text>}
|
</> : <Text>Sin datos</Text>}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
@@ -216,33 +295,30 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center',
|
splashContainer: { flex: 1, backgroundColor: '#1a7a4a', alignItems: 'center', justifyContent: 'center' },
|
||||||
justifyContent: 'center', padding: 24 },
|
splashEmoji: { fontSize: 72 },
|
||||||
|
splashTitle: { fontSize: 36, fontWeight: 'bold', color: '#fff', marginTop: 16 },
|
||||||
|
container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center', justifyContent: 'center', padding: 24 },
|
||||||
|
bigEmoji: { fontSize: 64, marginBottom: 8 },
|
||||||
title: { fontSize: 28, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' },
|
title: { fontSize: 28, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' },
|
||||||
subtitle: { fontSize: 15, color: '#555', marginBottom: 24, textAlign: 'center' },
|
subtitle: { fontSize: 15, color: '#555', marginBottom: 24, textAlign: 'center' },
|
||||||
input: { width: '100%', backgroundColor: '#fff', borderRadius: 10, padding: 14,
|
input: { width: '100%', backgroundColor: '#fff', borderRadius: 10, padding: 14, fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#ddd' },
|
||||||
fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#ddd' },
|
btn: { width: '100%', backgroundColor: '#1a7a4a', borderRadius: 10, padding: 16, alignItems: 'center', marginTop: 8 },
|
||||||
btn: { width: '100%', backgroundColor: '#1a7a4a', borderRadius: 10,
|
|
||||||
padding: 16, alignItems: 'center', marginTop: 8 },
|
|
||||||
btnText: { color: '#fff', fontWeight: 'bold', fontSize: 16 },
|
btnText: { color: '#fff', fontWeight: 'bold', fontSize: 16 },
|
||||||
btnSecondary: { width: '100%', borderRadius: 10, padding: 16,
|
btnSecondary: { width: '100%', borderRadius: 10, padding: 16, alignItems: 'center', marginTop: 8, borderWidth: 1, borderColor: '#1a7a4a' },
|
||||||
alignItems: 'center', marginTop: 8, borderWidth: 1, borderColor: '#1a7a4a' },
|
|
||||||
btnSecondaryText: { color: '#1a7a4a', fontWeight: 'bold', fontSize: 15 },
|
btnSecondaryText: { color: '#1a7a4a', fontWeight: 'bold', fontSize: 15 },
|
||||||
hint: { fontSize: 11, color: '#888', marginBottom: 16, textAlign: 'center' },
|
hint: { fontSize: 11, color: '#888', marginBottom: 16, textAlign: 'center' },
|
||||||
etaCard: { width: '100%', backgroundColor: '#fff', borderRadius: 16,
|
logoutText: { color: '#e53935', fontSize: 14, textAlign: 'center' },
|
||||||
padding: 20, marginBottom: 16, borderWidth: 1, borderColor: '#d0e8d8' },
|
etaCard: { width: '100%', backgroundColor: '#fff', borderRadius: 16, padding: 20, marginBottom: 16, borderWidth: 1, borderColor: '#d0e8d8' },
|
||||||
etaEvento: { fontSize: 18, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 8 },
|
etaEvento: { fontSize: 18, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 8 },
|
||||||
etaMensaje: { fontSize: 15, color: '#333', marginBottom: 16, lineHeight: 22 },
|
etaMensaje: { fontSize: 15, color: '#333', marginBottom: 16, lineHeight: 22 },
|
||||||
ventanaBox: { backgroundColor: '#e8f5ee', borderRadius: 10, padding: 14, marginBottom: 12 },
|
ventanaBox: { backgroundColor: '#e8f5ee', borderRadius: 10, padding: 14, marginBottom: 12 },
|
||||||
ventanaLabel: { fontSize: 12, color: '#555', marginBottom: 4 },
|
ventanaLabel: { fontSize: 12, color: '#555', marginBottom: 4 },
|
||||||
ventanaHora: { fontSize: 24, fontWeight: 'bold', color: '#1a7a4a' },
|
ventanaHora: { fontSize: 24, fontWeight: 'bold', color: '#1a7a4a' },
|
||||||
coloniaText: { fontSize: 12, color: '#888' },
|
coloniaText: { fontSize: 12, color: '#888' },
|
||||||
privacyBox: { width: '100%', backgroundColor: '#fff8e1', borderRadius: 10,
|
privacyBox: { width: '100%', backgroundColor: '#fff8e1', borderRadius: 10, padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#ffe082' },
|
||||||
padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#ffe082' },
|
|
||||||
privacyText: { fontSize: 12, color: '#795548', textAlign: 'center' },
|
privacyText: { fontSize: 12, color: '#795548', textAlign: 'center' },
|
||||||
separacionCard: { width: '100%', backgroundColor: '#fff', borderRadius: 12,
|
separacionCard: { width: '100%', backgroundColor: '#fff', borderRadius: 12, padding: 16, marginBottom: 12, flexDirection: 'row', alignItems: 'center', gap: 14, borderWidth: 1, borderColor: '#ddd' },
|
||||||
padding: 16, marginBottom: 12, flexDirection: 'row', alignItems: 'center',
|
|
||||||
gap: 14, borderWidth: 1, borderColor: '#ddd' },
|
|
||||||
separacionEmoji: { fontSize: 32 },
|
separacionEmoji: { fontSize: 32 },
|
||||||
separacionTipo: { fontSize: 16, fontWeight: 'bold', color: '#333' },
|
separacionTipo: { fontSize: 16, fontWeight: 'bold', color: '#333' },
|
||||||
separacionEjemplos: { fontSize: 12, color: '#666', marginTop: 2 },
|
separacionEjemplos: { fontSize: 12, color: '#666', marginTop: 2 },
|
||||||
|
|||||||
Reference in New Issue
Block a user