final
This commit is contained in:
parent
35f04423c4
commit
e67e1abdfb
88
backend/controllers/authController.js
Normal file
88
backend/controllers/authController.js
Normal file
@ -0,0 +1,88 @@
|
||||
const { User } = require('../models');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
// @desc Register a new user
|
||||
// @route POST /api/auth/register
|
||||
// @access Public
|
||||
exports.register = async (req, res) => {
|
||||
try {
|
||||
const { name, email, password } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const userExists = await User.findOne({ where: { email } });
|
||||
if (userExists) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'El usuario ya existe'
|
||||
});
|
||||
}
|
||||
|
||||
// Create user
|
||||
const user = await User.create({
|
||||
name,
|
||||
email,
|
||||
password, // Will be hashed by the model hooks
|
||||
profilePicUrl: '/uploads/default-avatar.jpg',
|
||||
notificationsEnabled: true
|
||||
});
|
||||
|
||||
// Return user without password
|
||||
const userResponse = user.toJSON();
|
||||
delete userResponse.password;
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
user: userResponse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error en register:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error al registrar usuario',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user
|
||||
// @route POST /api/auth/login
|
||||
// @access Public
|
||||
exports.login = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check if user exists
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Credenciales inválidas'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if password matches
|
||||
const isMatch = await user.matchPassword(password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Credenciales inválidas'
|
||||
});
|
||||
}
|
||||
|
||||
// Return user without password
|
||||
const userResponse = user.toJSON();
|
||||
delete userResponse.password;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
user: userResponse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error en login:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error al iniciar sesión',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
9
backend/routes/authRoutes.js
Normal file
9
backend/routes/authRoutes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const { register, login } = require('../controllers/authController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/register', register);
|
||||
router.post('/login', login);
|
||||
|
||||
module.exports = router;
|
||||
@ -14,6 +14,7 @@ const userRoutes = require('./routes/userRoutes');
|
||||
const gymClassRoutes = require('./routes/gymClassRoutes');
|
||||
const bookingRoutes = require('./routes/bookingRoutes');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
|
||||
// Initialize express app
|
||||
const app = express();
|
||||
@ -41,6 +42,7 @@ app.use('/api/users', userRoutes);
|
||||
app.use('/api/classes', gymClassRoutes);
|
||||
app.use('/api/bookings', bookingRoutes);
|
||||
app.use('/api/upload', uploadRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// Root route
|
||||
app.get('/', (req, res) => {
|
||||
|
||||
453
cap1.md
Normal file
453
cap1.md
Normal file
@ -0,0 +1,453 @@
|
||||
# Taller: Desarrollo con Angular e Ionic
|
||||
|
||||
## Capítulo 1: Introducción a Angular e Ionic
|
||||
|
||||
## 1. ¿Qué es Angular?
|
||||
|
||||
Angular es un framework de desarrollo front-end mantenido por Google, diseñado para crear aplicaciones web de una sola página (SPA) y aplicaciones móviles. Algunas características clave:
|
||||
|
||||
- **Basado en componentes**: Toda la interfaz se construye mediante componentes reutilizables
|
||||
- **TypeScript**: Utiliza TypeScript como lenguaje principal, añadiendo tipos estáticos a JavaScript
|
||||
- **Completo**: Ofrece soluciones integradas para enrutamiento, formularios, HTTP, animaciones, etc.
|
||||
- **Modular**: Su arquitectura permite dividir la aplicación en módulos funcionales
|
||||
- **Reactivo**: Facilita la programación reactiva mediante RxJS
|
||||
|
||||
Angular utiliza un sistema de "detección de cambios" para mantener sincronizada la interfaz de usuario con el estado de la aplicación, lo que permite crear interfaces dinámicas y reactivas.
|
||||
|
||||
## 2. Angular CLI: La Herramienta de Línea de Comandos
|
||||
|
||||
El Angular CLI (Command Line Interface) es una herramienta oficial que simplifica enormemente el desarrollo con Angular:
|
||||
|
||||
```bash
|
||||
# Instalación global
|
||||
npm install -g @angular/cli
|
||||
|
||||
# Crear nuevo proyecto
|
||||
ng new mi-proyecto
|
||||
|
||||
# Generar componentes, servicios, etc.
|
||||
ng generate component mi-componente
|
||||
ng generate service mi-servicio
|
||||
|
||||
# Iniciar servidor de desarrollo
|
||||
ng serve
|
||||
|
||||
# Construir para producción
|
||||
ng build --prod
|
||||
```
|
||||
|
||||
Beneficios del CLI:
|
||||
|
||||
- **Scaffolding**: Generación de código con estructura y configuración correctas
|
||||
- **Herramientas de desarrollo**: Servidor local con recarga en vivo (live reload)
|
||||
- **Optimización**: Empaquetado y minificación para producción
|
||||
- **Testing**: Configuración automática para pruebas unitarias e integración
|
||||
- **Actualización**: Facilita la actualización entre versiones de Angular
|
||||
|
||||
## 3. Angular vs Desarrollo Web Tradicional
|
||||
|
||||
Angular se asemeja al desarrollo de páginas web normales, pero con una estructura y enfoque diferentes:
|
||||
|
||||
| Desarrollo Web Tradicional | Desarrollo con Angular |
|
||||
|---------------------------|------------------------|
|
||||
| Páginas HTML separadas | Aplicación de una sola página (SPA) |
|
||||
| jQuery para manipular DOM | Vinculación de datos bidireccional |
|
||||
| JavaScript vanilla | TypeScript con tipos estáticos |
|
||||
| Recarga completa entre páginas | Navegación sin recarga (enrutamiento SPA) |
|
||||
| Mezcla de lógica y presentación | Separación modelo-vista-controlador |
|
||||
| Scripts y estilos globales | Encapsulación de componentes |
|
||||
|
||||
A pesar de las diferencias, los conocimientos de HTML, CSS y JavaScript son totalmente aplicables en Angular, ya que seguimos trabajando con estos lenguajes fundamentales pero de manera estructurada.
|
||||
|
||||
## 4. Componentes Básicos de Angular
|
||||
|
||||
#### Componentes
|
||||
|
||||
El elemento fundamental de las aplicaciones Angular:
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contador',
|
||||
template: `
|
||||
<div>
|
||||
<h2>Contador: {{ contador }}</h2>
|
||||
<button (click)="incrementar()">+</button>
|
||||
<button (click)="decrementar()">-</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
div { text-align: center; }
|
||||
button { margin: 0 5px; }
|
||||
`]
|
||||
})
|
||||
export class ContadorComponent {
|
||||
contador = 0;
|
||||
|
||||
incrementar() {
|
||||
this.contador++;
|
||||
}
|
||||
|
||||
decrementar() {
|
||||
this.contador--;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Módulos
|
||||
|
||||
Organizan la aplicación en bloques funcionales:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ContadorComponent } from './contador/contador.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ContadorComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
```
|
||||
|
||||
#### Servicios
|
||||
|
||||
Encapsulan la lógica de negocio y son inyectables en componentes:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DatosService {
|
||||
private datos = ['Primer elemento', 'Segundo elemento'];
|
||||
|
||||
getDatos() {
|
||||
return this.datos;
|
||||
}
|
||||
|
||||
agregarDato(dato: string) {
|
||||
this.datos.push(dato);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Directivas
|
||||
|
||||
Modifican el comportamiento del DOM:
|
||||
|
||||
```html
|
||||
<!-- Directiva estructural *ngFor -->
|
||||
<ul>
|
||||
<li *ngFor="let item of items">{{ item }}</li>
|
||||
</ul>
|
||||
|
||||
<!-- Directiva estructural *ngIf -->
|
||||
<div *ngIf="mostrarContenido">
|
||||
Este contenido es condicional
|
||||
</div>
|
||||
|
||||
<!-- Directiva de atributo -->
|
||||
<p [ngStyle]="{'color': color, 'font-size': tamano + 'px'}">
|
||||
Texto con estilo dinámico
|
||||
</p>
|
||||
```
|
||||
|
||||
#### Pipes
|
||||
|
||||
Transforman datos para su visualización:
|
||||
|
||||
```html
|
||||
<p>{{ fecha | date:'dd/MM/yyyy' }}</p>
|
||||
<p>{{ precio | currency:'EUR' }}</p>
|
||||
<p>{{ nombre | uppercase }}</p>
|
||||
<p>{{ textoLargo | slice:0:50 }}...</p>
|
||||
```
|
||||
|
||||
## 5. ¿Qué es Ionic?
|
||||
|
||||
Ionic es un framework de desarrollo de aplicaciones móviles híbridas que se construye sobre Angular:
|
||||
|
||||
- **Multiplataforma**: Una única base de código para iOS, Android y web
|
||||
- **Componentes nativos**: UI con apariencia y comportamiento nativo
|
||||
- **Capacitor/Cordova**: Acceso a APIs nativas del dispositivo
|
||||
- **Rendimiento optimizado**: Aplicaciones rápidas y responsivas
|
||||
|
||||
## 6. Componentes UI de Ionic
|
||||
|
||||
Ionic ofrece una amplia biblioteca de componentes que siguen las directrices de diseño de iOS y Android:
|
||||
|
||||
#### Navegación y Estructura
|
||||
|
||||
```html
|
||||
<!-- Tabs -->
|
||||
<ion-tabs>
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="home">
|
||||
<ion-icon name="home"></ion-icon>
|
||||
<ion-label>Inicio</ion-label>
|
||||
</ion-tab-button>
|
||||
<ion-tab-button tab="search">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
<ion-label>Buscar</ion-label>
|
||||
</ion-tab-button>
|
||||
<ion-tab-button tab="profile">
|
||||
<ion-icon name="person"></ion-icon>
|
||||
<ion-label>Perfil</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
|
||||
<!-- Menú lateral -->
|
||||
<ion-menu side="start" menuId="first">
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Menú</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Inicio</ion-item>
|
||||
<ion-item>Perfil</ion-item>
|
||||
<ion-item>Configuración</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
```
|
||||
|
||||
> **Nota:** Para ver ejemplos interactivos de estos componentes, visite la documentación oficial:
|
||||
> - [Ionic Tabs](https://ionicframework.com/docs/api/tabs)
|
||||
> - [Ionic Menu](https://ionicframework.com/docs/api/menu)
|
||||
|
||||
#### Componentes Básicos
|
||||
|
||||
```html
|
||||
<!-- Botones -->
|
||||
<ion-button>Botón Estándar</ion-button>
|
||||
<ion-button fill="outline">Botón Outline</ion-button>
|
||||
<ion-button expand="block" color="success">Botón Block</ion-button>
|
||||
|
||||
<!-- Cards -->
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Subtítulo</ion-card-subtitle>
|
||||
<ion-card-title>Título Principal</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
Contenido detallado de la tarjeta que puede incluir
|
||||
texto, imágenes y otros elementos.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<!-- Listas -->
|
||||
<ion-list>
|
||||
<ion-list-header>Usuarios</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-avatar slot="start">
|
||||
<img src="avatar.png">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h2>Juan Pérez</h2>
|
||||
<p>Desarrollador</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-avatar slot="start">
|
||||
<img src="avatar2.png">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h2>María García</h2>
|
||||
<p>Diseñadora</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
> **Nota:** Puede visualizar estos componentes en la documentación oficial:
|
||||
> - [Ionic Buttons](https://ionicframework.com/docs/api/button)
|
||||
> - [Ionic Cards](https://ionicframework.com/docs/api/card)
|
||||
> - [Ionic Lists](https://ionicframework.com/docs/api/list)
|
||||
|
||||
#### Formularios y Entrada
|
||||
|
||||
```html
|
||||
<!-- Inputs básicos -->
|
||||
<ion-item>
|
||||
<ion-label position="floating">Nombre</ion-label>
|
||||
<ion-input type="text"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label position="floating">Email</ion-label>
|
||||
<ion-input type="email"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<!-- Selectores -->
|
||||
<ion-item>
|
||||
<ion-label>Categoría</ion-label>
|
||||
<ion-select>
|
||||
<ion-select-option value="deportes">Deportes</ion-select-option>
|
||||
<ion-select-option value="musica">Música</ion-select-option>
|
||||
<ion-select-option value="tecnologia">Tecnología</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<!-- Toggle y checkbox -->
|
||||
<ion-item>
|
||||
<ion-label>Notificaciones</ion-label>
|
||||
<ion-toggle slot="end"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Acepto términos</ion-label>
|
||||
<ion-checkbox slot="start"></ion-checkbox>
|
||||
</ion-item>
|
||||
```
|
||||
|
||||
> **Nota:** Consulte la documentación oficial para ver ejemplos interactivos:
|
||||
> - [Ionic Input](https://ionicframework.com/docs/api/input)
|
||||
> - [Ionic Select](https://ionicframework.com/docs/api/select)
|
||||
> - [Ionic Toggle](https://ionicframework.com/docs/api/toggle)
|
||||
> - [Ionic Checkbox](https://ionicframework.com/docs/api/checkbox)
|
||||
|
||||
#### Feedback y Alertas
|
||||
|
||||
```typescript
|
||||
import { AlertController, ToastController, LoadingController } from '@ionic/angular';
|
||||
|
||||
constructor(
|
||||
private alertCtrl: AlertController,
|
||||
private toastCtrl: ToastController,
|
||||
private loadingCtrl: LoadingController
|
||||
) {}
|
||||
|
||||
async mostrarAlerta() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Alerta',
|
||||
subHeader: 'Información importante',
|
||||
message: '¿Estás seguro de realizar esta acción?',
|
||||
buttons: ['Cancelar', 'Aceptar']
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
async mostrarToast() {
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Operación completada con éxito',
|
||||
duration: 2000,
|
||||
position: 'bottom',
|
||||
color: 'success'
|
||||
});
|
||||
toast.present();
|
||||
}
|
||||
|
||||
async mostrarCargando() {
|
||||
const loading = await this.loadingCtrl.create({
|
||||
message: 'Cargando datos...',
|
||||
duration: 2000
|
||||
});
|
||||
await loading.present();
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota:** Para más información sobre alertas y elementos interactivos, consulte:
|
||||
> - [Ionic Alert](https://ionicframework.com/docs/api/alert)
|
||||
> - [Ionic Toast](https://ionicframework.com/docs/api/toast)
|
||||
> - [Ionic Loading](https://ionicframework.com/docs/api/loading)
|
||||
|
||||
## 7. Capacitor: El Puente Nativo
|
||||
|
||||
Capacitor es el framework que permite a Ionic acceder a las capacidades nativas del dispositivo:
|
||||
|
||||
- Cámara y galería de fotos
|
||||
- Geolocalización
|
||||
- Almacenamiento persistente
|
||||
- Notificaciones push
|
||||
- Sensores del dispositivo
|
||||
- Archivos y sistema de archivos
|
||||
|
||||
Ejemplo básico de uso de Capacitor:
|
||||
|
||||
```typescript
|
||||
import { Camera, CameraResultType } from '@capacitor/camera';
|
||||
|
||||
async function tomarFoto() {
|
||||
const imagen = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Uri
|
||||
});
|
||||
|
||||
// Usar la imagen (imagen.webPath)
|
||||
const imagenUrl = imagen.webPath;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Ciclo de Desarrollo con Ionic y Angular
|
||||
|
||||
El proceso típico de desarrollo con Ionic y Angular incluye:
|
||||
|
||||
1. **Creación del proyecto**:
|
||||
```bash
|
||||
# Crear un nuevo proyecto (por defecto usa Angular)
|
||||
ionic start mi-proyecto-ionic
|
||||
|
||||
# Especificar el framework (angular, react, vue)
|
||||
ionic start mi-proyecto-ionic --type=angular
|
||||
```
|
||||
|
||||
2. **Desarrollo en navegador web**:
|
||||
```bash
|
||||
# Iniciar servidor de desarrollo con recarga en vivo
|
||||
ionic serve
|
||||
```
|
||||
|
||||
3. **Pruebas en dispositivo real**:
|
||||
```bash
|
||||
# Añadir plataforma Android
|
||||
ionic capacitor add android
|
||||
|
||||
# Ejecutar en dispositivo con recarga en vivo
|
||||
ionic capacitor run android --livereload
|
||||
```
|
||||
|
||||
4. **Compilación para producción**:
|
||||
```bash
|
||||
# Construir la aplicación optimizada
|
||||
ionic build --prod
|
||||
|
||||
# Copiar los archivos a las plataformas nativas
|
||||
npx cap copy
|
||||
|
||||
# Abrir el proyecto en Android Studio para ajustes finales
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
## Resumen
|
||||
|
||||
Angular e Ionic forman una poderosa combinación para desarrollar aplicaciones móviles multiplataforma:
|
||||
|
||||
- Angular proporciona la estructura, organización y lógica
|
||||
- Ionic aporta componentes UI con aspecto nativo
|
||||
- Capacitor permite acceder a características nativas del dispositivo
|
||||
- El desarrollo es similar al web tradicional, pero más estructurado y optimizado
|
||||
|
||||
En los siguientes capítulos, exploraremos más a fondo cada uno de estos conceptos y construiremos paso a paso una aplicación completa de reservas para un gimnasio, además de la creación básica de un proyecto vacío.
|
||||
|
||||
## Recursos y Documentación
|
||||
|
||||
- [Documentación oficial de Angular](https://angular.io/docs)
|
||||
- [Documentación de Ionic Framework](https://ionicframework.com/docs)
|
||||
- [Capacitor Docs](https://capacitorjs.com/docs)
|
||||
- [Angular CLI](https://cli.angular.io/)
|
||||
- [TypeScript](https://www.typescriptlang.org/docs/)
|
||||
768
cap2.md
Normal file
768
cap2.md
Normal file
@ -0,0 +1,768 @@
|
||||
# Taller: Desarrollo con Angular e Ionic
|
||||
|
||||
## Capítulo 2: Componentes y Estructura de una Aplicación Angular/Ionic
|
||||
|
||||
## 1. Estructura de una Aplicación Angular/Ionic
|
||||
|
||||
Al crear un nuevo proyecto de Ionic con Angular mediante el comando `ionic start`, se genera una estructura de carpetas organizada:
|
||||
|
||||
```
|
||||
mi-app/
|
||||
├── src/ # Código fuente principal
|
||||
│ ├── app/ # Lógica y componentes de la aplicación
|
||||
│ │ ├── app.component.ts
|
||||
│ │ ├── app.module.ts
|
||||
│ │ ├── app-routing.module.ts
|
||||
│ ├── assets/ # Recursos estáticos (imágenes, fuentes, etc.)
|
||||
│ ├── environments/ # Configuraciones por entorno (dev, prod)
|
||||
│ ├── theme/ # Variables globales de estilo (colores, etc.)
|
||||
│ ├── global.scss # Estilos globales
|
||||
│ ├── index.html # Archivo HTML raíz
|
||||
│ ├── main.ts # Punto de entrada de la aplicación
|
||||
├── angular.json # Configuración de Angular
|
||||
├── capacitor.config.ts # Configuración de Capacitor
|
||||
├── package.json # Dependencias y scripts
|
||||
├── tsconfig.json # Configuración de TypeScript
|
||||
|
||||
```
|
||||
|
||||
### Archivos Clave:
|
||||
|
||||
- **app.module.ts**: Módulo principal que configura la aplicación
|
||||
- **app-routing.module.ts**: Define las rutas de navegación
|
||||
- **app.component.ts**: Componente raíz que contiene toda la aplicación
|
||||
- **index.html**: Archivo HTML base donde se monta la aplicación
|
||||
- **main.ts**: Punto de entrada que arranca la aplicación Angular
|
||||
|
||||
## 2. Fundamentos de los Componentes Angular
|
||||
|
||||
### Anatomía de un Componente
|
||||
|
||||
Cada componente Angular consta de:
|
||||
|
||||
1. **Clase TypeScript**: Contiene la lógica y datos
|
||||
2. **Plantilla HTML**: Define la estructura visual
|
||||
3. **Estilos CSS**: Define la apariencia (opcional)
|
||||
4. **Metadatos**: Configuración mediante decorador @Component
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-producto', // Cómo se referencia en HTML
|
||||
templateUrl: './producto.component.html', // Plantilla HTML
|
||||
styleUrls: ['./producto.component.scss'] // Estilos
|
||||
})
|
||||
export class ProductoComponent implements OnInit {
|
||||
nombre: string = 'Smartphone';
|
||||
precio: number = 599.99;
|
||||
disponible: boolean = true;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
// Inicialización del componente
|
||||
}
|
||||
|
||||
aplicarDescuento(porcentaje: number): void {
|
||||
this.precio = this.precio * (1 - porcentaje/100);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Ciclo de Vida de los Componentes
|
||||
|
||||
Los componentes tienen un ciclo de vida gestionado por Angular:
|
||||
|
||||
1. **ngOnChanges**: Cuando cambian las propiedades de entrada (@Input)
|
||||
2. **ngOnInit**: Después de la primera inicialización
|
||||
3. **ngAfterViewInit**: Cuando la vista se ha inicializado
|
||||
4. **ngOnDestroy**: Justo antes de que Angular destruya el componente
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DatosService } from './datos.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ejemplo',
|
||||
template: '<div>{{datos}}</div>'
|
||||
})
|
||||
export class EjemploComponent implements OnInit, OnDestroy {
|
||||
datos: any[] = [];
|
||||
private suscripcion!: Subscription;
|
||||
|
||||
constructor(private datosService: DatosService) { }
|
||||
|
||||
ngOnInit() {
|
||||
// Perfecto para inicializar datos, suscripciones, etc.
|
||||
this.suscripcion = this.datosService.obtenerDatos()
|
||||
.subscribe(datos => {
|
||||
this.datos = datos;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Limpieza antes de destruir el componente
|
||||
if (this.suscripcion) {
|
||||
this.suscripcion.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 3. Vinculación de Datos (Data Binding)
|
||||
|
||||
Angular ofrece cuatro formas de vinculación de datos:
|
||||
|
||||
### Interpolación {{ }}
|
||||
|
||||
Muestra valores de propiedades en la plantilla:
|
||||
|
||||
```html
|
||||
<h1>{{ titulo }}</h1>
|
||||
<p>Precio: {{ precio | currency }}</p>
|
||||
<div>Estado: {{ disponible ? 'En stock' : 'Agotado' }}</div>
|
||||
|
||||
```
|
||||
|
||||
### Property Binding [ ]
|
||||
|
||||
Vincula propiedades HTML con valores del componente:
|
||||
|
||||
```html
|
||||
<img [src]="imagenUrl">
|
||||
<button [disabled]="!formularioValido">Enviar</button>
|
||||
<div [ngClass]="{'destacado': esDestacado, 'oculto': !visible}">
|
||||
Contenido con clases dinámicas
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
### Event Binding ( )
|
||||
|
||||
Responde a eventos del usuario:
|
||||
|
||||
```html
|
||||
<button (click)="agregarAlCarrito()">Agregar al carrito</button>
|
||||
<input (input)="actualizarBusqueda($event)">
|
||||
<form (submit)="enviarFormulario()">
|
||||
<!-- Campos del formulario -->
|
||||
</form>
|
||||
|
||||
```
|
||||
|
||||
### Two-Way Binding [( )]
|
||||
|
||||
Combina property binding y event binding para actualizar datos en ambas direcciones:
|
||||
|
||||
```html
|
||||
<input [(ngModel)]="nombreUsuario">
|
||||
|
||||
<!-- Equivalente a: -->
|
||||
<input [value]="nombreUsuario" (input)="nombreUsuario = $event.target.value">
|
||||
|
||||
```
|
||||
|
||||
> Nota: Para usar ngModel, debes importar FormsModule en tu módulo Angular.
|
||||
|
||||
## 4. Directivas en Angular
|
||||
|
||||
Las directivas son clases que extienden HTML con nueva funcionalidad.
|
||||
|
||||
### Directivas Estructurales
|
||||
|
||||
Modifican el DOM añadiendo o quitando elementos:
|
||||
|
||||
```html
|
||||
<!-- *ngIf: Condicional -->
|
||||
<div *ngIf="producto.disponible">
|
||||
El producto está disponible
|
||||
</div>
|
||||
|
||||
<!-- *ngFor: Repetición -->
|
||||
<ul>
|
||||
<li *ngFor="let producto of productos; let i = index">
|
||||
{{i+1}}. {{producto.nombre}} - {{producto.precio | currency}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- *ngSwitch: Condicional múltiple -->
|
||||
<div [ngSwitch]="rol">
|
||||
<div *ngSwitchCase="'admin'">Panel de administrador</div>
|
||||
<div *ngSwitchCase="'editor'">Panel de editor</div>
|
||||
<div *ngSwitchDefault>Panel de usuario</div>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
### Directivas de Atributo
|
||||
|
||||
Modifican la apariencia o comportamiento de elementos existentes:
|
||||
|
||||
```html
|
||||
<!-- ngClass: Clases dinámicas -->
|
||||
<div [ngClass]="{'activo': estaActivo, 'destacado': esDestacado}">
|
||||
Elemento con clases dinámicas
|
||||
</div>
|
||||
|
||||
<!-- ngStyle: Estilos dinámicos -->
|
||||
<div [ngStyle]="{'color': colorTexto, 'font-size.px': tamanoFuente}">
|
||||
Texto con estilo dinámico
|
||||
</div>
|
||||
|
||||
<!-- ngModel: Two-way binding (requiere FormsModule) -->
|
||||
<input [(ngModel)]="nombre" placeholder="Escribe tu nombre">
|
||||
|
||||
```
|
||||
|
||||
## 5. Componentes UI de Ionic - Visión General
|
||||
|
||||
Ionic proporciona una amplia gama de componentes UI pre-diseñados que siguen las guías de diseño de iOS y Android. A continuación, se muestra una visión general de las categorías principales:
|
||||
|
||||
### Categorías de Componentes
|
||||
|
||||
1. **Navegación**
|
||||
|
||||
- `ion-header`, `ion-toolbar`, `ion-buttons`
|
||||
- `ion-tabs`
|
||||
- `ion-menu`
|
||||
- `ion-back-button`
|
||||
2. **Contenido y Presentación**
|
||||
|
||||
- `ion-card` y componentes relacionados
|
||||
- `ion-list`, `ion-item`
|
||||
- `ion-grid`, `ion-row`, `ion-col`
|
||||
- `ion-avatar`, `ion-thumbnail`
|
||||
3. **Formularios e Inputs**
|
||||
|
||||
- `ion-input`, `ion-textarea`
|
||||
- `ion-select`, `ion-radio`, `ion-checkbox`
|
||||
- `ion-toggle`, `ion-range`
|
||||
- `ion-datetime`
|
||||
4. **Feedback al Usuario**
|
||||
|
||||
- `ion-loading`
|
||||
- `ion-toast`
|
||||
- `ion-alert`
|
||||
- `ion-action-sheet`
|
||||
|
||||
### Ejemplo Básico de Uso
|
||||
|
||||
```html
|
||||
<!-- Ejemplo de una página con algunos componentes básicos -->
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Mi Aplicación</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<ion-card-title>Bienvenido</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
Contenido de ejemplo para mostrar un card básico.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label>Elemento 1</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Elemento 2</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
||||
```
|
||||
|
||||
### Controladores de Componentes
|
||||
|
||||
Para componentes interactivos como alertas, modales o toasts, se utilizan controladores:
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { AlertController } from '@ionic/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ejemplo',
|
||||
templateUrl: './ejemplo.page.html',
|
||||
})
|
||||
export class EjemploPage {
|
||||
constructor(private alertController: AlertController) {}
|
||||
|
||||
async mostrarAlerta() {
|
||||
const alert = await this.alertController.create({
|
||||
header: 'Información',
|
||||
message: 'Esta es una alerta de ejemplo',
|
||||
buttons: ['OK']
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> **Nota:** Para más detalles sobre cada componente, consulta la [documentación oficial de Ionic](https://ionicframework.com/docs/components), donde encontrarás ejemplos completos de uso y todas las propiedades disponibles.
|
||||
|
||||
## 6. Servicios y Dependency Injection
|
||||
|
||||
Los servicios en Angular son clases que encapsulan lógica de negocio y compartir estado entre componentes. La inyección de dependencias facilita su uso.
|
||||
|
||||
### Creación de un Servicio
|
||||
|
||||
```bash
|
||||
ionic generate service services/productos
|
||||
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface Producto {
|
||||
id: number;
|
||||
nombre: string;
|
||||
precio: number;
|
||||
imagen: string;
|
||||
descripcion: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root' // Disponible en toda la aplicación
|
||||
})
|
||||
export class ProductosService {
|
||||
private apiUrl = 'https://mi-api.com/productos';
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
obtenerProductos(): Observable<Producto[]> {
|
||||
return this.http.get<Producto[]>(this.apiUrl);
|
||||
}
|
||||
|
||||
obtenerProductoPorId(id: number): Observable<Producto> {
|
||||
return this.http.get<Producto>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
buscarProductos(termino: string): Observable<Producto[]> {
|
||||
return this.http.get<Producto[]>(this.apiUrl).pipe(
|
||||
map(productos => productos.filter(p =>
|
||||
p.nombre.toLowerCase().includes(termino.toLowerCase())
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
agregarProducto(producto: Producto): Observable<Producto> {
|
||||
return this.http.post<Producto>(this.apiUrl, producto);
|
||||
}
|
||||
|
||||
actualizarProducto(producto: Producto): Observable<Producto> {
|
||||
return this.http.put<Producto>(`${this.apiUrl}/${producto.id}`, producto);
|
||||
}
|
||||
|
||||
eliminarProducto(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Uso del Servicio en un Componente
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ProductosService, Producto } from '../../services/productos.service';
|
||||
import { LoadingController } from '@ionic/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lista-productos',
|
||||
templateUrl: './lista-productos.page.html',
|
||||
styleUrls: ['./lista-productos.page.scss'],
|
||||
})
|
||||
export class ListaProductosPage implements OnInit {
|
||||
productos: Producto[] = [];
|
||||
cargando = false;
|
||||
error = false;
|
||||
|
||||
constructor(
|
||||
private productosService: ProductosService,
|
||||
private loadingController: LoadingController
|
||||
) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.cargarProductos();
|
||||
}
|
||||
|
||||
async cargarProductos() {
|
||||
this.cargando = true;
|
||||
|
||||
const loading = await this.loadingController.create({
|
||||
message: 'Cargando productos...'
|
||||
});
|
||||
await loading.present();
|
||||
|
||||
this.productosService.obtenerProductos().subscribe({
|
||||
next: (data) => {
|
||||
this.productos = data;
|
||||
this.error = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al cargar productos', error);
|
||||
this.error = true;
|
||||
},
|
||||
complete: () => {
|
||||
this.cargando = false;
|
||||
loading.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refrescarProductos(event: any) {
|
||||
this.productosService.obtenerProductos().subscribe({
|
||||
next: (data) => {
|
||||
this.productos = data;
|
||||
this.error = false;
|
||||
event.target.complete();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al refrescar productos', error);
|
||||
this.error = true;
|
||||
event.target.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buscarProductos(event: any) {
|
||||
const termino = event.detail.value;
|
||||
|
||||
if (termino && termino.trim() !== '') {
|
||||
this.productosService.buscarProductos(termino).subscribe(productos => {
|
||||
this.productos = productos;
|
||||
});
|
||||
} else {
|
||||
this.cargarProductos();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 7. Navegación y Enrutamiento
|
||||
|
||||
Angular Router permite definir rutas para navegar entre páginas:
|
||||
|
||||
```typescript
|
||||
// app-routing.module.ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'inicio',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'inicio',
|
||||
loadChildren: () => import('./pages/inicio/inicio.module').then(m => m.InicioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'productos',
|
||||
loadChildren: () => import('./pages/productos/productos.module').then(m => m.ProductosPageModule)
|
||||
},
|
||||
{
|
||||
path: 'producto/:id',
|
||||
loadChildren: () => import('./pages/detalle-producto/detalle-producto.module').then(m => m.DetalleProductoPageModule)
|
||||
},
|
||||
{
|
||||
path: 'carrito',
|
||||
loadChildren: () => import('./pages/carrito/carrito.module').then(m => m.CarritoPageModule)
|
||||
},
|
||||
{
|
||||
path: 'perfil',
|
||||
loadChildren: () => import('./pages/perfil/perfil.module').then(m => m.PerfilPageModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
loadChildren: () => import('./pages/not-found/not-found.module').then(m => m.NotFoundPageModule)
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
```
|
||||
|
||||
### Navegación Declarativa
|
||||
|
||||
```html
|
||||
<!-- Enlace simple -->
|
||||
<ion-button routerLink="/productos">Ver productos</ion-button>
|
||||
|
||||
<!-- Con parámetros -->
|
||||
<ion-item [routerLink]="['/producto', producto.id]">
|
||||
{{ producto.nombre }}
|
||||
</ion-item>
|
||||
|
||||
<!-- Con parámetros de consulta -->
|
||||
<a [routerLink]="['/productos']" [queryParams]="{categoria: 'electronica'}">
|
||||
Ver electrónica
|
||||
</a>
|
||||
|
||||
```
|
||||
|
||||
### Navegación Programática
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { Router, NavigationExtras } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navegacion',
|
||||
templateUrl: './navegacion.page.html',
|
||||
})
|
||||
export class NavegacionPage {
|
||||
constructor(private router: Router) { }
|
||||
|
||||
irAProductos() {
|
||||
this.router.navigate(['/productos']);
|
||||
}
|
||||
|
||||
verProducto(id: number) {
|
||||
this.router.navigate(['/producto', id]);
|
||||
}
|
||||
|
||||
filtrarProductos() {
|
||||
const navigationExtras: NavigationExtras = {
|
||||
queryParams: {
|
||||
categoria: 'electronica',
|
||||
orden: 'precio'
|
||||
}
|
||||
};
|
||||
this.router.navigate(['/productos'], navigationExtras);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Recibir Parámetros
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ProductosService, Producto } from '../../services/productos.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-detalle-producto',
|
||||
templateUrl: './detalle-producto.page.html',
|
||||
})
|
||||
export class DetalleProductoPage implements OnInit {
|
||||
producto: Producto | undefined;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private productosService: ProductosService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
// Parámetros de ruta
|
||||
this.route.paramMap.subscribe(params => {
|
||||
const id = Number(params.get('id'));
|
||||
if (id) {
|
||||
this.cargarProducto(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Parámetros de consulta
|
||||
this.route.queryParamMap.subscribe(params => {
|
||||
const vista = params.get('vista');
|
||||
console.log('Vista:', vista);
|
||||
});
|
||||
}
|
||||
|
||||
cargarProducto(id: number) {
|
||||
this.productosService.obtenerProductoPorId(id).subscribe(producto => {
|
||||
this.producto = producto;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 8. Integración con Capacitor
|
||||
|
||||
Para utilizar capacidades nativas con Capacitor, necesitas instalar y configurar plugins específicos.
|
||||
|
||||
### Ejemplo: Cámara
|
||||
|
||||
```bash
|
||||
npm install @capacitor/camera
|
||||
npx cap sync
|
||||
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
|
||||
|
||||
@Component({
|
||||
selector: 'app-camara',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Cámara</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button expand="block" (click)="tomarFoto()">
|
||||
<ion-icon name="camera" slot="start"></ion-icon>
|
||||
Tomar Foto
|
||||
</ion-button>
|
||||
|
||||
<div *ngIf="photoUrl" class="ion-margin-top">
|
||||
<img [src]="photoUrl" alt="Foto tomada">
|
||||
</div>
|
||||
</ion-content>
|
||||
`
|
||||
})
|
||||
export class CamaraPage {
|
||||
photoUrl: string | undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
async tomarFoto() {
|
||||
try {
|
||||
const photo = await Camera.getPhoto({4. Las directivas extienden HTML con funcionalidades dinámicas
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Uri,
|
||||
source: CameraSource.Camera
|
||||
});
|
||||
|
||||
// La propiedad webPath es la URL que se puede usar para mostrar la foto
|
||||
this.photoUrl = photo.webPath;
|
||||
} catch (error) {
|
||||
console.error('Error al tomar la foto', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Ejemplo: Geolocalización
|
||||
|
||||
```bash
|
||||
npm install @capacitor/geolocation
|
||||
npx cap sync
|
||||
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { Geolocation, Position } from '@capacitor/geolocation';
|
||||
|
||||
@Component({
|
||||
selector: 'app-geolocalizacion',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Ubicación</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button expand="block" (click)="obtenerUbicacion()">
|
||||
<ion-icon name="location" slot="start"></ion-icon>
|
||||
Obtener Ubicación
|
||||
</ion-button>
|
||||
|
||||
<ion-card *ngIf="coordenadas">
|
||||
<ion-card-header>
|
||||
<ion-card-title>Tu ubicación actual</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<p><strong>Latitud:</strong> {{ coordenadas.latitude }}</p>
|
||||
<p><strong>Longitud:</strong> {{ coordenadas.longitude }}</p>
|
||||
<p><strong>Precisión:</strong> {{ coordenadas.accuracy }} metros</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-content>
|
||||
`
|
||||
})
|
||||
export class GeolocalizacionPage {
|
||||
coordenadas: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
accuracy: number;
|
||||
} | undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
async obtenerUbicacion() {
|
||||
try {
|
||||
// Solicitar permisos primero
|
||||
const permisos = await Geolocation.checkPermissions();
|
||||
|
||||
if (permisos.location !== 'granted') {
|
||||
const solicitado = await Geolocation.requestPermissions();
|
||||
if (solicitado.location !== 'granted') {
|
||||
throw new Error('Permiso de ubicación denegado');
|
||||
}
|
||||
}
|
||||
|
||||
const position = await Geolocation.getCurrentPosition({
|
||||
enableHighAccuracy: true
|
||||
});
|
||||
|
||||
this.coordenadas = {
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error al obtener ubicación', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Resumen
|
||||
|
||||
En este capítulo hemos explorado a fondo los componentes y estructura de una aplicación Angular/Ionic:
|
||||
|
||||
1. La estructura de carpetas sigue una organización lógica que separa código, estilos y recursos
|
||||
2. Los componentes Angular encapsulan HTML, CSS y lógica en unidades coherentes
|
||||
3. El enlace de datos permite la comunicación bidireccional entre vistas y lógica
|
||||
4. Las directivas extienden HTML con funcionalidades dinámicas
|
||||
5. Ionic proporciona componentes UI que se adaptan a iOS y Android automáticamente
|
||||
6. Los servicios permiten compartir lógica y estado entre componentes
|
||||
7. El enrutamiento facilita la navegación entre diferentes vistas
|
||||
8. Capacitor extiende las aplicaciones con acceso a funcionalidades nativas
|
||||
|
||||
Con estos fundamentos, estamos listos para combinar todos estos conceptos en una aplicación completa en el siguiente capítulo.
|
||||
|
||||
## Recursos Adicionales
|
||||
|
||||
- [Guía de Componentes Angular](https://angular.io/guide/component-overview)
|
||||
- [Ciclo de Vida en Angular](https://angular.io/guide/lifecycle-hooks)
|
||||
- [Directivas en Angular](https://angular.io/guide/attribute-directives)
|
||||
- [Componentes UI de Ionic](https://ionicframework.com/docs/components)
|
||||
- [Servicios e Inyección de Dependencias](https://angular.io/guide/dependency-injection)
|
||||
- [Router de Angular](https://angular.io/guide/router)
|
||||
- [Documentación de Capacitor](https://capacitorjs.com/docs/apis)
|
||||
|
||||
287
cap3.md
Normal file
287
cap3.md
Normal file
@ -0,0 +1,287 @@
|
||||
# Taller: Desarrollo con Angular e Ionic
|
||||
|
||||
## Capítulo 3: Estructura de la Aplicación de Reservas para Gimnasio
|
||||
|
||||
En este capítulo, detallaremos la estructura y planificación de nuestra aplicación de reservas para gimnasio. Esto nos servirá como guía para el desarrollo que realizaremos en los siguientes capítulos.
|
||||
|
||||
## 1. Definición de Requisitos
|
||||
|
||||
Nuestra aplicación de reservas para gimnasio debe cumplir con los siguientes requisitos:
|
||||
|
||||
### Funcionalidades Principales
|
||||
|
||||
- **Ver clases disponibles**: Listado de clases con información sobre horarios, instructores y plazas
|
||||
- **Reservar clases**: Posibilidad de reservar una clase si hay plazas disponibles
|
||||
- **Gestionar reservas**: Ver, modificar y cancelar reservas existentes
|
||||
- **Perfil de usuario**: Información básica del usuario y preferencias
|
||||
- **Notificaciones**: Recordatorios de clases reservadas
|
||||
|
||||
### Tipos de Usuario
|
||||
|
||||
- **Clientes**: Pueden ver clases, hacer reservas y gestionar su perfil
|
||||
- **Administradores**: Pueden gestionar clases, instructores y ver estadísticas (para una versión futura)
|
||||
|
||||
## 2. Planificación de la Estructura
|
||||
|
||||
### Entidades Principales
|
||||
|
||||
1. **Clases (GymClass)**
|
||||
|
||||
- ID
|
||||
- Nombre
|
||||
- Descripción
|
||||
- Instructor
|
||||
- Horario (Inicio/Fin)
|
||||
- Capacidad máxima
|
||||
- Reservas actuales
|
||||
2. **Reservas (Booking)**
|
||||
|
||||
- ID
|
||||
- ID de Usuario
|
||||
- ID de Clase
|
||||
- Fecha
|
||||
- Estado (Confirmada, Cancelada, Pendiente)
|
||||
3. **Usuario (User)**
|
||||
|
||||
- ID
|
||||
- Nombre
|
||||
- Email
|
||||
- Foto de perfil
|
||||
- Preferencias
|
||||
|
||||
### Páginas Principales
|
||||
|
||||
1. **Inicio/Login**
|
||||
|
||||
- Autenticación de usuarios
|
||||
- Pantalla de bienvenida
|
||||
2. **Lista de Clases**
|
||||
|
||||
- Mostrar todas las clases disponibles
|
||||
- Filtros por día, tipo, instructor
|
||||
3. **Detalle de Clase**
|
||||
|
||||
- Información completa de la clase
|
||||
- Botón para reservar
|
||||
- Lista de participantes (opcional)
|
||||
4. **Mis Reservas**
|
||||
|
||||
- Lista de reservas del usuario
|
||||
- Opciones para cancelar/modificar
|
||||
5. **Perfil**
|
||||
|
||||
- Datos del usuario
|
||||
- Preferencias
|
||||
- Estadísticas (opcional)
|
||||
|
||||
## 3. Diseño de la Arquitectura
|
||||
|
||||
### Estructura de Carpetas
|
||||
|
||||
```
|
||||
gym-reservation-app/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── core/ # Servicios core, guards, interceptores
|
||||
│ │ │ ├── services/
|
||||
│ │ │ ├── guards/
|
||||
│ │ │ └── interceptors/
|
||||
│ │ ├── shared/ # Componentes compartidos
|
||||
│ │ │ ├── components/
|
||||
│ │ │ ├── directives/
|
||||
│ │ │ └── pipes/
|
||||
│ │ ├── models/ # Interfaces y modelos
|
||||
│ │ ├── pages/ # Páginas principales
|
||||
│ │ │ ├── auth/
|
||||
│ │ │ ├── classes/
|
||||
│ │ │ ├── class-detail/
|
||||
│ │ │ ├── bookings/
|
||||
│ │ │ └── profile/
|
||||
│ │ ├── app.component.ts
|
||||
│ │ ├── app.module.ts
|
||||
│ │ └── app-routing.module.ts
|
||||
│ ├── assets/
|
||||
│ │ ├── icon/
|
||||
│ │ └── images/
|
||||
│ ├── environments/
|
||||
│ └── theme/
|
||||
└── capacitor.config.ts
|
||||
|
||||
```
|
||||
|
||||
### Arquitectura y Patrones
|
||||
|
||||
Utilizaremos una arquitectura basada en:
|
||||
|
||||
1. **Componentes**: Encapsulan partes de la UI y su lógica asociada
|
||||
2. **Servicios**: Manejan la lógica de negocio y el acceso a datos
|
||||
3. **Modelos**: Definen las estructuras de datos
|
||||
4. **Enrutamiento**: Gestiona la navegación entre páginas
|
||||
5. **Patrón Observable**: Para gestionar flujos de datos asincrónicos
|
||||
|
||||
## 4. Diseño de la Base de Datos
|
||||
|
||||
Para esta versión inicial, utilizaremos almacenamiento local mediante Capacitor/Storage. En una versión futura, podríamos migrar a Firebase o una API REST.
|
||||
|
||||
**Estructura del almacenamiento local**:
|
||||
|
||||
```json
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Yoga",
|
||||
"description": "Clase de yoga para todos los niveles",
|
||||
"instructor": "María López",
|
||||
"startTime": "2025-04-22T08:00:00",
|
||||
"endTime": "2025-04-22T09:00:00",
|
||||
"maxCapacity": 15,
|
||||
"currentBookings": 8
|
||||
},
|
||||
// Más clases...
|
||||
],
|
||||
"bookings": [
|
||||
{
|
||||
"id": "1",
|
||||
"userId": "user123",
|
||||
"classId": "1",
|
||||
"className": "Yoga",
|
||||
"date": "2025-04-22T08:00:00",
|
||||
"status": "confirmed"
|
||||
},
|
||||
// Más reservas...
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"id": "user123",
|
||||
"name": "Usuario Demo",
|
||||
"email": "usuario@ejemplo.com",
|
||||
"profilePic": "avatar.jpg",
|
||||
"preferences": {
|
||||
"notifications": true,
|
||||
"favoriteClasses": ["1", "3"]
|
||||
}
|
||||
},
|
||||
// Más usuarios...
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 5. Flujos de Navegación
|
||||
|
||||
### Flujo Principal
|
||||
|
||||
1. **Inicio**: El usuario abre la aplicación
|
||||
2. **Autenticación**: El usuario inicia sesión (simulado en esta versión)
|
||||
3. **Tabs**: Navegación principal con tabs
|
||||
- Tab 1: Clases disponibles
|
||||
- Tab 2: Mis reservas
|
||||
- Tab 3: Perfil
|
||||
|
||||
### Flujo de Reserva
|
||||
|
||||
1. Usuario navega a "Clases disponibles"
|
||||
2. Usuario selecciona una clase específica
|
||||
3. En la pantalla de detalle, pulsa "Reservar Clase"
|
||||
4. Se muestra confirmación y se redirige a "Mis Reservas"
|
||||
|
||||
### Flujo de Cancelación
|
||||
|
||||
1. Usuario navega a "Mis Reservas"
|
||||
2. Usuario desliza una reserva hacia la izquierda
|
||||
3. Pulsa en "Cancelar"
|
||||
4. Se muestra confirmación y se actualiza la lista
|
||||
|
||||
|
||||
|
||||
## 6. Integración con Capacitor
|
||||
|
||||
Para mejorar la experiencia de usuario y aprovechar las capacidades nativas, integraremos los siguientes plugins de Capacitor:
|
||||
|
||||
1. **@capacitor/local-notifications**
|
||||
|
||||
- Enviar recordatorios de clases
|
||||
- Notificaciones de confirmación de reservas
|
||||
2. **@capacitor/camera**
|
||||
|
||||
- Permitir a los usuarios actualizar su foto de perfil
|
||||
- Opción de tomar foto o seleccionar de la galería
|
||||
3. **@capacitor/preferences (antes Storage)**
|
||||
|
||||
- Almacenar datos de usuario
|
||||
- Guardar preferencias
|
||||
- Persistir información de clases y reservas
|
||||
4. **@capacitor/device** (opcional)
|
||||
|
||||
- Obtener información del dispositivo para personalizar la experiencia
|
||||
|
||||
## 7. Plan de Implementación
|
||||
|
||||
Para desarrollar la aplicación seguiremos estos pasos:
|
||||
|
||||
1. **Configuración del proyecto**
|
||||
|
||||
- Crear proyecto Ionic con Angular
|
||||
- Instalar dependencias necesarias
|
||||
2. **Implementación de los modelos de datos**
|
||||
|
||||
- Definir interfaces TypeScript para las entidades
|
||||
3. **Servicios Core**
|
||||
|
||||
- Implementar servicios para gestionar clases
|
||||
- Implementar servicios para gestionar reservas
|
||||
- Implementar servicio de autenticación (mock)
|
||||
4. **Componentes Shared**
|
||||
|
||||
- Crear componentes reutilizables (tarjetas, listas, etc.)
|
||||
5. **Páginas principales**
|
||||
|
||||
- Implementar página de listado de clases
|
||||
- Implementar página de detalle de clase
|
||||
- Implementar página de mis reservas
|
||||
- Implementar página de perfil
|
||||
6. **Navegación y enrutamiento**
|
||||
|
||||
- Configurar las rutas
|
||||
- Implementar navegación entre páginas
|
||||
7. **Capacitor y funcionalidades nativas**
|
||||
|
||||
- Integrar notificaciones
|
||||
- Integrar cámara
|
||||
- Implementar almacenamiento persistente
|
||||
8. **Pruebas y ajustes**
|
||||
|
||||
- Probar funcionalidades
|
||||
- Ajustar estilos y mejorar UX
|
||||
|
||||
## 8. Consideraciones para Versiones Futuras
|
||||
|
||||
En futuras versiones se podrían incluir:
|
||||
|
||||
1. **Autenticación real**
|
||||
|
||||
- Integración con Firebase Auth o similares
|
||||
- Roles de usuario (cliente/admin)
|
||||
2. **Backend real**
|
||||
|
||||
- API REST o Firebase para datos en tiempo real
|
||||
- Sincronización entre dispositivos
|
||||
3. **Funcionalidades avanzadas**
|
||||
|
||||
- Pagos para clases premium
|
||||
- Calendarios personalizados
|
||||
- Estadísticas de asistencia
|
||||
- Chat con instructores
|
||||
4. **Mejoras UX/UI**
|
||||
|
||||
- Temas personalizables
|
||||
- Animaciones
|
||||
- Modo oscuro/claro
|
||||
|
||||
## Conclusión
|
||||
|
||||
En este capítulo hemos definido la estructura de nuestra aplicación de reservas para gimnasio, estableciendo una base sólida para el desarrollo. Con esta planificación, podemos proceder a la implementación paso a paso, siguiendo las mejores prácticas de Angular e Ionic.
|
||||
|
||||
En el siguiente capítulo, comenzaremos con la implementación práctica, creando el proyecto base y desarrollando los modelos y servicios esenciales.
|
||||
|
||||
941
cap4.md
Normal file
941
cap4.md
Normal file
@ -0,0 +1,941 @@
|
||||
# Capítulo 4: Desarrollo Práctico de la Aplicación de Reservas
|
||||
|
||||
En este capítulo comenzaremos el desarrollo práctico de nuestra aplicación de reservas para gimnasio utilizando Ionic y Angular. Seguiremos paso a paso la implementación desde cero.
|
||||
|
||||
## 1. Creación y Configuración del Proyecto
|
||||
|
||||
### Crear el proyecto
|
||||
|
||||
Lo primero es crear el proyecto mediante el CLI de Ionic:
|
||||
|
||||
> Es necesario recordar de que se tiene que tener node instalado, en este caso yo personalmente estoy usando la versión LTS de node (22.14)
|
||||
|
||||
```bash
|
||||
# Asegúrate de tener instalado Ionic CLI y Angular CLI
|
||||
npm install -g @ionic/cli @angular/cli
|
||||
|
||||
# Crear el proyecto con el template de tabs
|
||||
ionic start taller tabs --type=angular
|
||||
|
||||
# Entrar al directorio del proyecto
|
||||
cd taller
|
||||
```
|
||||
|
||||
![\[img\]https://i.ibb.co/Xx1kxbMn/instalar-cli.png\[/img\]](https://i.ibb.co/Xx1kxbMn/instalar-cli.png)
|
||||

|
||||
|
||||
> Últimamente Angular impulsa mucho el uso de los conocidos como `componentes independientes`, personalmente no me gustan mucho, así que para efectos de la demostración lo haré con la versión clásica con módulos
|
||||
|
||||
### Estructura inicial del proyecto
|
||||
|
||||
Veamos la estructura de carpetas generada:
|
||||
|
||||
```
|
||||
taller/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── tab1/ # Tab 1 generado automáticamente
|
||||
│ │ ├── tab2/ # Tab 2 generado automáticamente
|
||||
│ │ ├── tab3/ # Tab 3 generado automáticamente
|
||||
│ │ ├── tabs/ # Controlador de tabs
|
||||
│ │ ├── app-routing.module.ts
|
||||
│ │ ├── app.component.ts
|
||||
│ │ └── app.module.ts
|
||||
│ ├── assets/
|
||||
│ ├── environments/
|
||||
│ ├── theme/
|
||||
│ ├── global.scss
|
||||
│ ├── index.html
|
||||
│ └── main.ts
|
||||
├── angular.json
|
||||
├── capacitor.config.ts
|
||||
├── package.json
|
||||
└── ...
|
||||
|
||||
```
|
||||
|
||||
### Ejecutar el proyecto
|
||||
|
||||
Iniciemos la aplicación para verificar que todo funciona correctamente:
|
||||
|
||||
```bash
|
||||
ionic serve
|
||||
```
|
||||
|
||||
> Literalmente usa exactamente el mismo servidor de desarrollo que una típica aplicación de Angular, aunque lo único que cambia en este punto es el puerto por defecto, ahora es el 8100 en vez del 4200, de igual manera se puede cambiar sin problemas
|
||||
|
||||

|
||||
|
||||

|
||||
Deberías ver la aplicación por defecto en tu navegador, con tres tabs básicos.
|
||||
|
||||
## 2. Organización del Proyecto
|
||||
|
||||
Vamos a reorganizar la estructura para adaptarla a nuestras necesidades:
|
||||
|
||||
```bash
|
||||
# Crear directorios para nuestra estructura, puedes crearlos manualmente,
|
||||
# o en mi caso que estoy usando una distribución linux los haré con estos comandos
|
||||
mkdir -p src/app/pages
|
||||
mkdir -p src/app/services
|
||||
mkdir -p src/app/models
|
||||
mkdir -p src/app/shared/components
|
||||
```
|
||||
|
||||
## 3. Definición de Modelos
|
||||
|
||||
Creemos los modelos de datos que necesitaremos:
|
||||
|
||||
```bash
|
||||
# Crear archivos para los modelos
|
||||
# lo mismo de arriba, puedes crearlos manualmente, en mi caso usaré estos comandos
|
||||
touch src/app/models/gym-class.model.ts
|
||||
touch src/app/models/booking.model.ts
|
||||
touch src/app/models/user.model.ts
|
||||
```
|
||||
|
||||
Ahora implementemos las interfaces para cada modelo:
|
||||
|
||||
**gym-class.model.ts**:
|
||||
|
||||
```typescript
|
||||
export interface GymClass {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructor: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
maxCapacity: number;
|
||||
currentBookings: number;
|
||||
category?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**booking.model.ts**:
|
||||
|
||||
```typescript
|
||||
export interface Booking {
|
||||
id: string;
|
||||
userId: string;
|
||||
classId: string;
|
||||
className: string;
|
||||
date: Date;
|
||||
status: 'confirmed' | 'cancelled' | 'pending';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**user.model.ts**:
|
||||
|
||||
```typescript
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
profilePic?: string;
|
||||
profilePicUrl?: string; // Para compatibilidad con el backend
|
||||
notificationsEnabled?: boolean; // Para compatibilidad con el backend
|
||||
preferences?: {
|
||||
notifications: boolean;
|
||||
favoriteClasses?: string[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Creación de Servicios
|
||||
|
||||
Ahora, implementaremos los servicios que gestionarán los datos:
|
||||
|
||||
```bash
|
||||
# Generar servicios usando el CLI de Ionic
|
||||
ionic generate service services/classes
|
||||
ionic generate service services/bookings
|
||||
ionic generate service services/auth
|
||||
ionic generate service services/storage
|
||||
ionic generate service services/upload
|
||||
ionic generate service services/notification
|
||||
```
|
||||
|
||||
### Implementación del Servicio de Almacenamiento
|
||||
|
||||
Primero vamos a instalar el plugin de Preferences de Capacitor:
|
||||
|
||||
> Este plugin es básicamente para guardar datos o caché de nuestra app, en caso de que la exportemos para móviles como Android o iOS
|
||||
|
||||
```bash
|
||||
npm install @capacitor/preferences
|
||||
```
|
||||
|
||||
Ahora implementemos el servicio de almacenamiento:
|
||||
|
||||
**services/storage.service.ts**:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
async set(key: string, value: any): Promise<void> {
|
||||
await Preferences.set({
|
||||
key,
|
||||
value: JSON.stringify(value)
|
||||
});
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
const { value } = await Preferences.get({ key });
|
||||
|
||||
if (value) {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
await Preferences.remove({ key });
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await Preferences.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Configuración de Entornos
|
||||
|
||||
Configuremos los archivos de entorno para facilitar el cambio entre desarrollo y producción:
|
||||
|
||||
**src/environments/environment.ts**:
|
||||
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'http://localhost:3000' // URL de la API de desarrollo
|
||||
};
|
||||
```
|
||||
|
||||
**src/environments/environment.prod.ts**:
|
||||
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl: 'https://taller.ionic.lcespedes.dev' // URL de la API de producción
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Integración con Backend
|
||||
|
||||
Nuestra aplicación se conectará a un backend real en lugar de utilizar datos mockeados. Instalemos las dependencias necesarias:
|
||||
|
||||
```bash
|
||||
npm install @angular/common/http
|
||||
```
|
||||
|
||||
### Configuración del Módulo HTTP
|
||||
|
||||
Agreguemos el módulo HTTP al app.module.ts:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouteReuseStrategy } from '@angular/router';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
IonicModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
HttpClientModule
|
||||
],
|
||||
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## 7. Implementación de Autenticación
|
||||
|
||||
Crearemos un módulo de autenticación completo:
|
||||
|
||||
```bash
|
||||
ionic generate page pages/auth
|
||||
ionic generate component pages/auth/components/login
|
||||
ionic generate component pages/auth/components/register
|
||||
ionic generate guard pages/auth/guards/auth
|
||||
```
|
||||
|
||||
### Implementación del Guard de Autenticación
|
||||
|
||||
**src/app/pages/auth/guards/auth.guard.ts**:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
// Esperar a que el servicio de autenticación esté inicializado
|
||||
await this.authService.waitForInitialization();
|
||||
|
||||
if (this.authService.isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redirigir al login si no hay sesión
|
||||
this.router.navigateByUrl('/auth');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuración de Rutas Principales
|
||||
|
||||
Actualizaremos el app-routing.module.ts para implementar las rutas correctas:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from './pages/auth/guards/auth.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'auth',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
|
||||
},
|
||||
{
|
||||
path: 'classes',
|
||||
loadChildren: () => import('./pages/classes/classes.module').then(m => m.ClassesPageModule)
|
||||
},
|
||||
{
|
||||
path: 'class-detail',
|
||||
loadChildren: () => import('./pages/class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
|
||||
},
|
||||
{
|
||||
path: 'bookings',
|
||||
loadChildren: () => import('./pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
loadChildren: () => import('./pages/profile/profile.module').then(m => m.ProfilePageModule)
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
```
|
||||
|
||||
## 8. Creación de Páginas Principales
|
||||
|
||||
Vamos a crear las páginas principales de nuestra aplicación:
|
||||
|
||||
```bash
|
||||
# Eliminar las páginas de tabs generadas automáticamente
|
||||
rm -rf src/app/tab1
|
||||
rm -rf src/app/tab2
|
||||
rm -rf src/app/tab3
|
||||
|
||||
# Generar nuestras propias páginas
|
||||
ionic generate page pages/classes
|
||||
ionic generate page pages/class-detail
|
||||
ionic generate page pages/bookings
|
||||
ionic generate page pages/profile
|
||||
```
|
||||
|
||||
### Configuración de las rutas y tabs
|
||||
|
||||
Actualizamos el archivo de rutas de tabs:
|
||||
|
||||
**src/app/tabs/tabs-routing.module.ts**:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { TabsPage } from './tabs.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: TabsPage,
|
||||
children: [
|
||||
{
|
||||
path: 'classes',
|
||||
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
|
||||
},
|
||||
{
|
||||
path: 'bookings',
|
||||
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/app/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/app/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
})
|
||||
export class TabsPageRoutingModule {}
|
||||
```
|
||||
|
||||
Actualizamos el template de tabs:
|
||||
|
||||
**src/app/tabs/tabs.page.html**:
|
||||
|
||||
```html
|
||||
<ion-tabs>
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="classes">
|
||||
<ion-icon name="calendar"></ion-icon>
|
||||
<ion-label>Clases</ion-label>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="bookings">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
<ion-label>Mis Reservas</ion-label>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="profile">
|
||||
<ion-icon name="person"></ion-icon>
|
||||
<ion-label>Perfil</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
```
|
||||
|
||||
## 9. Implementar Servicio de Autenticación
|
||||
|
||||
Ahora implementaremos el servicio de autenticación real que se comunica con el backend:
|
||||
|
||||
**services/auth.service.ts**:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { User } from '../models/user.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
// Clave para almacenar información de sesión
|
||||
const AUTH_TOKEN_KEY = 'auth_token';
|
||||
const USER_KEY = 'user_data';
|
||||
|
||||
// Interfaz para credenciales de login
|
||||
interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Interfaz para datos de registro
|
||||
interface RegisterData {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = `${environment.apiUrl}/api/users`;
|
||||
private authUrl = `${environment.apiUrl}/api/auth`;
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
// Intentar recuperar sesión anterior
|
||||
this.initializeSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa la sesión si hay datos guardados
|
||||
*/
|
||||
async initializeSession() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
const userData = await this.storageService.get(USER_KEY) as User | null;
|
||||
|
||||
if (userData) {
|
||||
console.log('Sesión recuperada del almacenamiento local:', userData);
|
||||
// Normalizar campos para compatibilidad
|
||||
this.normalizeUserData(userData);
|
||||
this.currentUserSubject.next(userData);
|
||||
} else {
|
||||
console.log('No hay sesión guardada, redirigiendo a login');
|
||||
this.currentUserSubject.next(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al inicializar sesión:', error);
|
||||
this.currentUserSubject.next(null);
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Espera a que se complete la inicialización
|
||||
* Importante para evitar redirecciones erróneas cuando la app inicia
|
||||
*/
|
||||
async waitForInitialization(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
// Si no se ha inicializado, iniciamos el proceso
|
||||
await this.initializeSession();
|
||||
|
||||
// Esperar hasta que se inicialice (con timeout)
|
||||
let attempts = 0;
|
||||
while (!this.isInitialized && attempts < 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza los datos de usuario para compatibilidad entre frontend/backend
|
||||
*/
|
||||
private normalizeUserData(user: User) {
|
||||
// Sincronizar campos de imagen
|
||||
if (user.profilePicUrl && !user.profilePic) {
|
||||
user.profilePic = user.profilePicUrl;
|
||||
} else if (user.profilePic && !user.profilePicUrl) {
|
||||
user.profilePicUrl = user.profilePic;
|
||||
}
|
||||
|
||||
// Sincronizar preferencias de notificaciones
|
||||
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
||||
user.preferences = {
|
||||
notifications: user.notificationsEnabled,
|
||||
favoriteClasses: []
|
||||
};
|
||||
} else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
|
||||
user.notificationsEnabled = user.preferences.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
return this.currentUserSubject.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iniciar sesión con email y contraseña
|
||||
*/
|
||||
login(credentials: LoginCredentials): Observable<User> {
|
||||
console.log('Intentando login con:', credentials);
|
||||
return this.http.post<any>(`${this.authUrl}/login`, credentials).pipe(
|
||||
tap(async (response) => {
|
||||
console.log('Respuesta de login:', response);
|
||||
|
||||
// Guardar token si el backend lo devuelve
|
||||
if (response.token) {
|
||||
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
||||
}
|
||||
|
||||
// Verificar y normalizar datos del usuario
|
||||
const user = response.user || response;
|
||||
this.normalizeUserData(user);
|
||||
|
||||
// Guardar datos del usuario
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
// Actualizar estado de sesión
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
map(response => response.user || response)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar un nuevo usuario
|
||||
*/
|
||||
register(userData: RegisterData): Observable<User> {
|
||||
console.log('Registrando usuario:', userData);
|
||||
const registerData = {
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
return this.http.post<any>(`${this.authUrl}/register`, registerData).pipe(
|
||||
tap(async (response) => {
|
||||
console.log('Respuesta de registro:', response);
|
||||
|
||||
// Guardar token si el backend lo devuelve
|
||||
if (response.token) {
|
||||
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
||||
}
|
||||
|
||||
// Verificar y normalizar datos del usuario
|
||||
const user = response.user || response;
|
||||
this.normalizeUserData(user);
|
||||
|
||||
// Guardar datos del usuario
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
// Actualizar estado de sesión
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
map(response => response.user || response)
|
||||
);
|
||||
}
|
||||
|
||||
updateUserProfile(userData: Partial<User>): Observable<User> {
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return of(currentUser as unknown as User);
|
||||
}
|
||||
|
||||
console.log('Actualizando perfil de usuario con:', userData);
|
||||
|
||||
// Asegurar que ambos campos de imagen estén sincronizados
|
||||
if (userData.profilePic && !userData.profilePicUrl) {
|
||||
userData.profilePicUrl = userData.profilePic;
|
||||
} else if (userData.profilePicUrl && !userData.profilePic) {
|
||||
userData.profilePic = userData.profilePicUrl;
|
||||
}
|
||||
|
||||
// Sincronizar notificaciones entre los dos formatos
|
||||
if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) {
|
||||
userData.notificationsEnabled = userData.preferences.notifications;
|
||||
} else if (userData.notificationsEnabled !== undefined &&
|
||||
(!userData.preferences || userData.preferences.notifications === undefined)) {
|
||||
if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] };
|
||||
userData.preferences.notifications = userData.notificationsEnabled;
|
||||
}
|
||||
|
||||
// Solo enviamos al servidor los campos que espera
|
||||
const backendUserData = {
|
||||
name: userData.name,
|
||||
profilePicUrl: userData.profilePicUrl,
|
||||
notificationsEnabled: userData.notificationsEnabled ||
|
||||
(userData.preferences ? userData.preferences.notifications : undefined)
|
||||
};
|
||||
|
||||
return this.http.put<User>(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
|
||||
tap(async (user) => {
|
||||
console.log('Usuario actualizado correctamente:', user);
|
||||
|
||||
// Sincronizar campos para mantener compatibilidad
|
||||
if (user.profilePicUrl && !user.profilePic) {
|
||||
user.profilePic = user.profilePicUrl;
|
||||
} else if (user.profilePic && !user.profilePicUrl) {
|
||||
user.profilePicUrl = user.profilePic;
|
||||
}
|
||||
|
||||
// Mantener los campos que espera el frontend
|
||||
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
||||
user.preferences = {
|
||||
notifications: user.notificationsEnabled,
|
||||
favoriteClasses: currentUser.preferences?.favoriteClasses || []
|
||||
};
|
||||
}
|
||||
|
||||
// Actualizar almacenamiento local
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error al actualizar usuario:', error);
|
||||
// Devolver el usuario actualizado localmente para que la UI no se rompa
|
||||
// en caso de error de red
|
||||
const updatedUser = { ...currentUser, ...userData };
|
||||
this.currentUserSubject.next(updatedUser);
|
||||
return of(updatedUser);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar sesión y eliminar datos
|
||||
*/
|
||||
async logout(): Promise<boolean> {
|
||||
try {
|
||||
// En una aplicación real, enviaríamos petición al backend
|
||||
// this.http.post(`${this.authUrl}/logout`, {}).subscribe();
|
||||
|
||||
// Limpiar datos de sesión
|
||||
await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
|
||||
|
||||
// Actualizar estado
|
||||
this.currentUserSubject.next(null);
|
||||
this.isInitialized = false; // Permitir nueva inicialización
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error al cerrar sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si hay sesión activa
|
||||
*/
|
||||
isLoggedIn(): boolean {
|
||||
return !!this.currentUserSubject.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Creación de Componentes de Login y Registro
|
||||
|
||||
Implementemos los componentes de autenticación:
|
||||
|
||||
**pages/auth/components/login/login.component.html**:
|
||||
|
||||
```html
|
||||
<div class="auth-container">
|
||||
<h2>Iniciar Sesión</h2>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Email</ion-label>
|
||||
<ion-input type="email" formControlName="email"></ion-input>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('required')">
|
||||
Email es requerido
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('email')">
|
||||
Formato de email inválido
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Contraseña</ion-label>
|
||||
<ion-input type="password" formControlName="password"></ion-input>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('required')">
|
||||
Contraseña es requerida
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('minlength')">
|
||||
La contraseña debe tener al menos 6 caracteres
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
||||
Iniciar Sesión
|
||||
</ion-button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
**pages/auth/components/register/register.component.html**:
|
||||
|
||||
```html
|
||||
<div class="auth-container">
|
||||
<h2>Crear Cuenta</h2>
|
||||
|
||||
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Nombre</ion-label>
|
||||
<ion-input type="text" formControlName="name"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('name')?.touched && registerForm.get('name')?.hasError('required')">
|
||||
Nombre es requerido
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Email</ion-label>
|
||||
<ion-input type="email" formControlName="email"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('required')">
|
||||
Email es requerido
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('email')">
|
||||
Formato de email inválido
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Contraseña</ion-label>
|
||||
<ion-input type="password" formControlName="password"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('required')">
|
||||
Contraseña es requerida
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('minlength')">
|
||||
La contraseña debe tener al menos 6 caracteres
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
||||
Registrarse
|
||||
</ion-button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
**pages/auth/auth.page.html**:
|
||||
|
||||
```html
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Gym Reservation</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="logo-container">
|
||||
<img src="assets/shapes.svg" alt="Logo" class="logo">
|
||||
<h1>Gym Reservation</h1>
|
||||
<p>Tu app para reservar clases en tu gimnasio favorito</p>
|
||||
</div>
|
||||
|
||||
<ion-segment [(ngModel)]="segment" (ionChange)="segmentChanged($event)">
|
||||
<ion-segment-button value="login">
|
||||
<ion-label>Iniciar Sesión</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="register">
|
||||
<ion-label>Registrarse</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
|
||||
<div [ngSwitch]="segment">
|
||||
<app-login *ngSwitchCase="'login'" (loginSuccess)="onLoginSuccess()"></app-login>
|
||||
<app-register *ngSwitchCase="'register'" (registerSuccess)="onRegisterSuccess()"></app-register>
|
||||
</div>
|
||||
|
||||
<div class="demo-access">
|
||||
<p>¿Quieres probar la app? Usa estos datos:</p>
|
||||
<p><strong>Email:</strong> usuario@ejemplo.com</p>
|
||||
<p><strong>Contraseña:</strong> password123</p>
|
||||
</div>
|
||||
</ion-content>
|
||||
```
|
||||
|
||||
## 11. Servicio para Subir Imágenes
|
||||
|
||||
Implementemos un servicio para manejar la carga de imágenes al backend:
|
||||
|
||||
**services/upload.service.ts**:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UploadService {
|
||||
private apiUrl = `${environment.apiUrl}/api/upload`;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
/**
|
||||
* Sube una imagen al servidor
|
||||
* @param file Archivo a subir
|
||||
* @param type Tipo de imagen (avatar, class, etc)
|
||||
* @returns URL de la imagen subida
|
||||
*/
|
||||
uploadImage(file: File, type: 'avatar' | 'class' = 'avatar'): Observable<{ imageUrl: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('type', type);
|
||||
|
||||
return this.http.post<{ imageUrl: string }>(`${this.apiUrl}`, formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte una imagen dataURL (base64) a un archivo File
|
||||
* @param dataUrl URL de datos base64
|
||||
* @param filename Nombre del archivo
|
||||
*/
|
||||
dataURLtoFile(dataUrl: string, filename: string): File {
|
||||
const arr = dataUrl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
|
||||
return new File([u8arr], filename, { type: mime });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resumen
|
||||
|
||||
En este capítulo, hemos establecido las bases sólidas para nuestra aplicación de reservas para gimnasio:
|
||||
|
||||
1. Creamos la estructura del proyecto
|
||||
2. Implementamos los modelos de datos
|
||||
3. Configuramos los servicios básicos
|
||||
4. Establecimos la autenticación con un backend real
|
||||
5. Implementamos las rutas principales
|
||||
6. Creamos las páginas y componentes esenciales
|
||||
7. Integramos el sistema de subida de imágenes
|
||||
|
||||
En el siguiente capítulo, completaremos la implementación con:
|
||||
- Página de listado de clases
|
||||
- Detalle de clase y reserva
|
||||
- Gestión de reservas
|
||||
- Perfil de usuario
|
||||
- Integración con capacidades nativas mediante Capacitor
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> **Nota importante:** Si durante el desarrollo encuentras errores o discrepancias en la ejecución del código, verifica la implementación correcta de cada módulo. El código completo de cada componente está disponible en el repositorio del curso. Asegúrate de revisar que los nombres de las clases, servicios e importaciones coincidan exactamente con lo especificado en este tutorial.
|
||||
>
|
||||
> Si tienes problemas con algún módulo específico, puedes verificar la estructura del proyecto y consultar la documentación oficial de Ionic y Angular para obtener más información sobre cómo resolver los errores más comunes.
|
||||
> Adicionalmente igual puedes descargar el repositorio completo desde este [link](https://git.lcespedes.dev/lxluxo23/taller-ionic)
|
||||
> Digo eso ya que es en ocasiones el ionic-cli funciona de manera distinta dependiendo de la versión de este mismo, además de la versión de node.
|
||||
711
cap6.md
Normal file
711
cap6.md
Normal file
@ -0,0 +1,711 @@
|
||||
# Capítulo 6: Estructura del Backend y Despliegue de la Aplicación
|
||||
|
||||
En este capítulo, exploraremos la estructura del backend que hemos implementado para nuestra aplicación de reservas de gimnasio y el proceso de despliegue para dispositivos móviles reales.
|
||||
|
||||
## 1. Arquitectura del Backend
|
||||
|
||||
Nuestra aplicación ahora cuenta con un backend real basado en Node.js, Express y PostgreSQL que gestiona todas las operaciones de datos. Este enfoque tiene varias ventajas sobre el almacenamiento local:
|
||||
|
||||
- Datos centralizados y accesibles desde cualquier dispositivo
|
||||
- Mayor seguridad y control de acceso
|
||||
- Escalabilidad mejorada para manejar más usuarios
|
||||
- Sincronización de datos en tiempo real
|
||||
|
||||
### Estructura de Carpetas del Backend
|
||||
|
||||
```
|
||||
backend/
|
||||
├── config/
|
||||
│ └── db.js # Configuración de la base de datos
|
||||
├── controllers/
|
||||
│ ├── authController.js # Controlador de autenticación
|
||||
│ ├── bookingController.js # Controlador de reservas
|
||||
│ ├── gymClassController.js # Controlador de clases
|
||||
│ ├── uploadController.js # Controlador para subida de archivos
|
||||
│ └── userController.js # Controlador de usuarios
|
||||
├── models/
|
||||
│ ├── Booking.js # Modelo de datos para reservas
|
||||
│ ├── GymClass.js # Modelo de datos para clases
|
||||
│ ├── User.js # Modelo de datos para usuarios
|
||||
│ └── index.js # Exporta todos los modelos
|
||||
├── routes/
|
||||
│ ├── authRoutes.js # Rutas de autenticación
|
||||
│ ├── bookingRoutes.js # Rutas de reservas
|
||||
│ ├── gymClassRoutes.js # Rutas de clases
|
||||
│ ├── uploadRoutes.js # Rutas para subida de archivos
|
||||
│ └── userRoutes.js # Rutas de usuarios
|
||||
├── uploads/ # Directorio para archivos subidos
|
||||
├── server.js # Punto de entrada principal
|
||||
└── package.json # Dependencias y scripts
|
||||
```
|
||||
|
||||
### Tecnologías Utilizadas en el Backend
|
||||
|
||||
- **Node.js**: Entorno de ejecución JavaScript del lado del servidor
|
||||
- **Express**: Framework web para crear APIs REST
|
||||
- **PostgreSQL**: Base de datos relacional robusta para almacenar los datos
|
||||
- **Sequelize**: ORM para modelar los datos y conectar con PostgreSQL
|
||||
- **JWT (JSON Web Tokens)**: Para gestionar la autenticación de usuarios
|
||||
- **Multer**: Para manejar la subida de archivos
|
||||
|
||||
## 2. Implementación del Backend
|
||||
|
||||
### Configuración de la Base de Datos
|
||||
|
||||
**config/db.js**:
|
||||
|
||||
```javascript
|
||||
const { Sequelize } = require('sequelize');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Create Sequelize instance
|
||||
let sequelize;
|
||||
|
||||
if (process.env.DATABASE_URL) {
|
||||
// Use connection string if available
|
||||
sequelize = new Sequelize(process.env.DATABASE_URL, {
|
||||
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||
define: {
|
||||
freezeTableName: true // Prevent Sequelize from pluralizing table names
|
||||
},
|
||||
dialectOptions: {
|
||||
ssl: {
|
||||
require: true,
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fall back to individual parameters
|
||||
sequelize = new Sequelize(
|
||||
process.env.DB_NAME || 'postgres',
|
||||
process.env.DB_USER || 'postgres',
|
||||
process.env.DB_PASSWORD || 'postgres',
|
||||
{
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
dialect: 'postgres',
|
||||
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||
define: {
|
||||
freezeTableName: true // Prevent Sequelize from pluralizing table names
|
||||
},
|
||||
dialectOptions: {
|
||||
ssl: process.env.NODE_ENV === 'production' ? {
|
||||
require: true,
|
||||
rejectUnauthorized: false
|
||||
} : false
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connection established successfully.');
|
||||
} catch (error) {
|
||||
console.error('Unable to connect to the database:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { sequelize, connectDB };
|
||||
```
|
||||
|
||||
### Modelos de Datos
|
||||
|
||||
**models/User.js**:
|
||||
|
||||
```javascript
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/db');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const User = sequelize.define('user', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
profilePicUrl: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: '/uploads/default-avatar.jpg'
|
||||
},
|
||||
notificationsEnabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
}
|
||||
}, {
|
||||
hooks: {
|
||||
beforeCreate: async (user) => {
|
||||
if (user.password) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
}
|
||||
},
|
||||
beforeUpdate: async (user) => {
|
||||
if (user.changed('password')) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
}
|
||||
}
|
||||
},
|
||||
tableName: 'users' // Explicitly set lowercase table name
|
||||
});
|
||||
|
||||
// Method to check if password matches
|
||||
User.prototype.matchPassword = async function(enteredPassword) {
|
||||
return await bcrypt.compare(enteredPassword, this.password);
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
```
|
||||
|
||||
**models/GymClass.js**:
|
||||
|
||||
```javascript
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/db');
|
||||
|
||||
const GymClass = sequelize.define('gymClass', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
instructor: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
maxCapacity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
currentBookings: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'Otros'
|
||||
},
|
||||
imageUrl: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: '/uploads/default-class.jpg'
|
||||
}
|
||||
}, {
|
||||
tableName: 'classes' // Explicitly set lowercase table name
|
||||
});
|
||||
|
||||
module.exports = GymClass;
|
||||
```
|
||||
|
||||
**models/Booking.js**:
|
||||
|
||||
```javascript
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/db');
|
||||
|
||||
const Booking = sequelize.define('booking', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'classes',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
className: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
date: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('confirmed', 'cancelled', 'pending'),
|
||||
defaultValue: 'confirmed'
|
||||
}
|
||||
}, {
|
||||
tableName: 'bookings' // Explicitly set lowercase table name
|
||||
});
|
||||
|
||||
module.exports = Booking;
|
||||
```
|
||||
|
||||
**models/index.js**:
|
||||
|
||||
```javascript
|
||||
const User = require('./User');
|
||||
const GymClass = require('./GymClass');
|
||||
const Booking = require('./Booking');
|
||||
|
||||
// Define associations
|
||||
User.hasMany(Booking, { foreignKey: 'userId' });
|
||||
Booking.belongsTo(User, { foreignKey: 'userId' });
|
||||
|
||||
GymClass.hasMany(Booking, { foreignKey: 'classId' });
|
||||
Booking.belongsTo(GymClass, { foreignKey: 'classId' });
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
GymClass,
|
||||
Booking
|
||||
};
|
||||
```
|
||||
|
||||
### Controladores
|
||||
|
||||
**controllers/authController.js**:
|
||||
|
||||
```javascript
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models');
|
||||
|
||||
// Generate JWT Token
|
||||
const generateToken = (id) => {
|
||||
return jwt.sign({ id }, process.env.JWT_SECRET || 'secret_key_12345', {
|
||||
expiresIn: '30d'
|
||||
});
|
||||
};
|
||||
|
||||
// @desc Register a new user
|
||||
// @route POST /api/auth/register
|
||||
// @access Public
|
||||
exports.registerUser = async (req, res) => {
|
||||
try {
|
||||
const { name, email, password } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ where: { email } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email is already registered'
|
||||
});
|
||||
}
|
||||
|
||||
// Create user
|
||||
const user = await User.create({
|
||||
name,
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profilePicUrl: user.profilePicUrl,
|
||||
notificationsEnabled: user.notificationsEnabled
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering user:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user
|
||||
// @route POST /api/auth/login
|
||||
// @access Public
|
||||
exports.loginUser = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check for user email
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if password matches
|
||||
const isMatch = await user.matchPassword(password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profilePicUrl: user.profilePicUrl,
|
||||
notificationsEnabled: user.notificationsEnabled
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error logging in:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Rutas API
|
||||
|
||||
**routes/authRoutes.js**:
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const { registerUser, loginUser } = require('../controllers/authController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/register', registerUser);
|
||||
router.post('/login', loginUser);
|
||||
|
||||
module.exports = router;
|
||||
```
|
||||
|
||||
### Punto de Entrada del Servidor
|
||||
|
||||
**server.js**:
|
||||
|
||||
```javascript
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const dotenv = require('dotenv');
|
||||
const cors = require('cors');
|
||||
const morgan = require('morgan');
|
||||
const fs = require('fs');
|
||||
const { connectDB, sequelize } = require('./config/db');
|
||||
|
||||
// Load env variables
|
||||
dotenv.config();
|
||||
|
||||
// Import route files
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const gymClassRoutes = require('./routes/gymClassRoutes');
|
||||
const bookingRoutes = require('./routes/bookingRoutes');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
|
||||
// Initialize express app
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
|
||||
// Dev logging middleware
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
// Create uploads directory if it doesn't exist
|
||||
const uploadsDir = path.join(__dirname, 'uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Set static folder for uploads
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// Mount routers
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/classes', gymClassRoutes);
|
||||
app.use('/api/bookings', bookingRoutes);
|
||||
app.use('/api/upload', uploadRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// Root route
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Welcome to Gym API' });
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({
|
||||
message: 'Server Error',
|
||||
error: err.message
|
||||
});
|
||||
});
|
||||
|
||||
// Define port
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Connect to database and start server
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB();
|
||||
|
||||
await sequelize.sync();
|
||||
console.log('Database synchronized');
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unable to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.log(`Error: ${err.message}`);
|
||||
// Close server & exit process
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## 3. Despliegue en Plataformas Móviles
|
||||
|
||||
Una de las grandes ventajas de usar Ionic con Capacitor es la posibilidad de desplegar la misma base de código en múltiples plataformas. Vamos a revisar el proceso para Android.
|
||||
|
||||
### Preparación para Android
|
||||
|
||||
1. **Instalar las Dependencias de Android**:
|
||||
- Android Studio
|
||||
- JDK (Java Development Kit)
|
||||
- Configurar variables de entorno (ANDROID_HOME, JAVA_HOME)
|
||||
|
||||
2. **Añadir la Plataforma Android a nuestro Proyecto**:
|
||||
|
||||
```bash
|
||||
# Instalar @capacitor/android si no está instalado
|
||||
npm install @capacitor/android
|
||||
|
||||
# Añadir Android al proyecto
|
||||
npx cap add android
|
||||
```
|
||||
|
||||
3. **Sincronizar el Proyecto**:
|
||||
|
||||
Después de hacer cambios en el código web, necesitamos construir el proyecto y sincronizarlo con las plataformas nativas:
|
||||
|
||||
```bash
|
||||
# Construir la aplicación
|
||||
ionic build --prod
|
||||
|
||||
# Sincronizar con Android
|
||||
npx cap sync android
|
||||
```
|
||||
|
||||
4. **Abrir el Proyecto en Android Studio**:
|
||||
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
### Configuración del AndroidManifest.xml
|
||||
|
||||
El archivo `AndroidManifest.xml` es crucial para definir los permisos y configuraciones de la aplicación Android. Ya cubrimos algunos permisos esenciales:
|
||||
|
||||
```xml
|
||||
<!-- Permisos para cámara -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
|
||||
<!-- Permisos para notificaciones -->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
```
|
||||
|
||||
### Personalización de Iconos y Splash Screen
|
||||
|
||||
Para personalizar los iconos y la pantalla de inicio:
|
||||
|
||||
1. **Reemplazar los Archivos de Iconos**:
|
||||
- Crear iconos para diferentes resoluciones
|
||||
- Reemplazar los archivos en `android/app/src/main/res/mipmap-*/`
|
||||
|
||||
2. **Reemplazar la Pantalla de Inicio (Splash Screen)**:
|
||||
- Crear imágenes para diferentes orientaciones y resoluciones
|
||||
- Reemplazar los archivos en `android/app/src/main/res/drawable-*/splash.png`
|
||||
|
||||
3. **Personalizar el Nombre de la Aplicación**:
|
||||
- Editar `android/app/src/main/res/values/strings.xml`:
|
||||
```xml
|
||||
<resources>
|
||||
<string name="app_name">Gym Reservation</string>
|
||||
<string name="title_activity_main">Gym Reservation</string>
|
||||
<string name="package_name">com.example.gymreservation</string>
|
||||
<string name="custom_url_scheme">com.example.gymreservation</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
### Construir un APK de Prueba
|
||||
|
||||
Para generar un APK que puedas compartir con probadores:
|
||||
|
||||
1. En Android Studio, selecciona **Build > Build Bundle(s) / APK(s) > Build APK(s)**
|
||||
2. Cuando termine, haz clic en **locate** para encontrar el archivo APK
|
||||
3. Este archivo se puede compartir directamente para instalarlo en dispositivos Android (se debe habilitar la instalación desde fuentes desconocidas)
|
||||
|
||||
### Construir un Bundle para Google Play
|
||||
|
||||
Para publicar en la tienda de Google Play:
|
||||
|
||||
1. En Android Studio, selecciona **Build > Generate Signed Bundle / APK**
|
||||
2. Elige **Android App Bundle**
|
||||
3. Crea o selecciona una clave de firma (keystore)
|
||||
4. Completa la información necesaria y genera el bundle (.aab)
|
||||
5. Este archivo es el que se sube a la Google Play Console
|
||||
|
||||
## 4. Consideraciones de Seguridad
|
||||
|
||||
Al desplegar una aplicación móvil que se comunica con un backend, es crucial considerar aspectos de seguridad:
|
||||
|
||||
### Autenticación Segura
|
||||
|
||||
Nuestra aplicación implementa JWT (JSON Web Tokens) para autenticar usuarios, pero aquí hay algunas mejoras adicionales:
|
||||
|
||||
- **Tokens de Actualización**: Implementar tokens de corta duración con tokens de actualización
|
||||
- **Almacenamiento Seguro**: Utilizar almacenamiento nativo seguro para tokens
|
||||
- **HTTPS**: Garantizar que todas las comunicaciones API usen HTTPS
|
||||
|
||||
### Manejo de Datos Sensibles
|
||||
|
||||
- **No almacenar contraseñas en texto plano**: Ya estamos usando bcrypt para hash de contraseñas
|
||||
- **Minimizar los datos almacenados localmente**: Solo guardar lo necesario en el dispositivo
|
||||
- **Eliminar datos al cerrar sesión**: Limpiar datos sensibles cuando el usuario cierre sesión
|
||||
|
||||
### Permisos de Aplicación
|
||||
|
||||
- **Solicitar solo los permisos necesarios**: No solicitar permisos que no sean esenciales
|
||||
- **Explicar por qué se necesitan permisos**: Proporcionar contexto cuando se solicitan permisos
|
||||
- **Manejar denegaciones graciosamente**: La aplicación debe funcionar aunque se denieguen permisos no críticos
|
||||
|
||||
## 5. Optimizaciones de Rendimiento
|
||||
|
||||
Para garantizar que nuestra aplicación funcione de manera óptima en dispositivos reales:
|
||||
|
||||
### Optimización de Imágenes
|
||||
|
||||
- **Redimensionar imágenes**: Utilizar tamaños apropiados para dispositivos móviles
|
||||
- **Compresión de imágenes**: Reducir el tamaño de archivo sin perder calidad visible
|
||||
- **Carga progresiva**: Implementar carga progresiva para imágenes grandes
|
||||
|
||||
### Optimización de Red
|
||||
|
||||
- **Implementar caché**: Almacenar en caché respuestas de API para reducir solicitudes de red
|
||||
- **Carga perezosa (lazy loading)**: Cargar datos solo cuando sean necesarios
|
||||
- **Compresión de datos**: Utilizar gzip en el servidor para respuestas API
|
||||
|
||||
### Optimización de la UI
|
||||
|
||||
- **Virtualización de listas**: Para listas largas, solo renderizar elementos visibles
|
||||
- **Animaciones eficientes**: Usar animaciones CSS en lugar de JavaScript cuando sea posible
|
||||
- **Evitar work blocking**: No bloquear el hilo principal con operaciones pesadas
|
||||
|
||||
## 6. Pruebas y Depuración
|
||||
|
||||
Para identificar y resolver problemas en dispositivos reales:
|
||||
|
||||
### Herramientas de Depuración
|
||||
|
||||
- **Chrome Remote Debugging**: Para depurar WebView en dispositivos Android
|
||||
- **Safari Web Inspector**: Para depurar WebView en dispositivos iOS
|
||||
- **Capacitor Logs**: Usar `npx cap logs android` para ver logs en tiempo real
|
||||
|
||||
### Pruebas en Dispositivos Reales
|
||||
|
||||
- **Probar en diferentes tamaños de pantalla**: Asegurar que la UI sea responsiva
|
||||
- **Probar con diferentes velocidades de red**: Simular conexiones lentas
|
||||
- **Pruebas de batería**: Verificar que la aplicación no consuma demasiada batería
|
||||
|
||||
## Resumen
|
||||
|
||||
En este capítulo, hemos explorado:
|
||||
|
||||
1. La arquitectura de nuestro backend Node.js/Express/PostgreSQL
|
||||
2. Los detalles de implementación de los modelos, controladores y rutas del backend
|
||||
3. El proceso de despliegue para dispositivos Android
|
||||
4. Consideraciones de seguridad para aplicaciones móviles
|
||||
5. Técnicas de optimización de rendimiento
|
||||
6. Herramientas y enfoques para pruebas y depuración
|
||||
|
||||
Con esto, nuestra aplicación Gym Reservation está lista para ser utilizada por usuarios reales, con un backend robusto y una experiencia móvil optimizada.
|
||||
4663
combinado.md
Normal file
4663
combinado.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from './pages/auth/guards/auth.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
|
||||
redirectTo: 'auth',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
|
||||
},
|
||||
{
|
||||
path: 'classes',
|
||||
|
||||
32
src/app/pages/auth/auth.module.ts
Normal file
32
src/app/pages/auth/auth.module.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AuthPage } from './auth.page';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { RegisterComponent } from './components/register/register.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AuthPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
declarations: [
|
||||
AuthPage,
|
||||
LoginComponent,
|
||||
RegisterComponent
|
||||
]
|
||||
})
|
||||
export class AuthPageModule {}
|
||||
33
src/app/pages/auth/auth.page.html
Normal file
33
src/app/pages/auth/auth.page.html
Normal file
@ -0,0 +1,33 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Gym Reservation</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="logo-container">
|
||||
<img src="assets/shapes.svg" alt="Logo" class="logo">
|
||||
<h1>Gym Reservation</h1>
|
||||
<p>Tu app para reservar clases en tu gimnasio favorito</p>
|
||||
</div>
|
||||
|
||||
<ion-segment [(ngModel)]="segment" (ionChange)="segmentChanged($event)">
|
||||
<ion-segment-button value="login">
|
||||
<ion-label>Iniciar Sesión</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="register">
|
||||
<ion-label>Registrarse</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
|
||||
<div [ngSwitch]="segment">
|
||||
<app-login *ngSwitchCase="'login'" (loginSuccess)="onLoginSuccess()"></app-login>
|
||||
<app-register *ngSwitchCase="'register'" (registerSuccess)="onRegisterSuccess()"></app-register>
|
||||
</div>
|
||||
|
||||
<div class="demo-access">
|
||||
<p>¿Quieres probar la app? Usa estos datos:</p>
|
||||
<p><strong>Email:</strong> usuario@ejemplo.com</p>
|
||||
<p><strong>Contraseña:</strong> password123</p>
|
||||
</div>
|
||||
</ion-content>
|
||||
40
src/app/pages/auth/auth.page.scss
Normal file
40
src/app/pages/auth/auth.page.scss
Normal file
@ -0,0 +1,40 @@
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.logo {
|
||||
max-width: 120px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--ion-color-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: var(--ion-color-medium);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-segment {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-access {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--ion-color-primary-rgb), 0.1);
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 14px;
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
}
|
||||
44
src/app/pages/auth/auth.page.ts
Normal file
44
src/app/pages/auth/auth.page.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { IonHeader } from "@ionic/angular/standalone";
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.page.html',
|
||||
styleUrls: ['./auth.page.scss'],
|
||||
standalone:false
|
||||
})
|
||||
export class AuthPage implements OnInit {
|
||||
segment = 'login';
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private navController: NavController
|
||||
) { }
|
||||
|
||||
async ngOnInit() {
|
||||
// Esperar a que se inicialice la sesión (importante)
|
||||
await this.authService.waitForInitialization();
|
||||
|
||||
// Si ya hay una sesión, redirigir a la app
|
||||
if (this.authService.isLoggedIn()) {
|
||||
console.log('Usuario autenticado, redirigiendo a la app...');
|
||||
this.navController.navigateRoot('/app/classes');
|
||||
} else {
|
||||
console.log('No hay sesión activa, mostrando pantalla de login');
|
||||
}
|
||||
}
|
||||
|
||||
segmentChanged(ev: any) {
|
||||
this.segment = ev.detail.value;
|
||||
}
|
||||
|
||||
onLoginSuccess() {
|
||||
this.navController.navigateRoot('/app/classes');
|
||||
}
|
||||
|
||||
onRegisterSuccess() {
|
||||
this.navController.navigateRoot('/app/classes');
|
||||
}
|
||||
}
|
||||
31
src/app/pages/auth/components/login/login.component.html
Normal file
31
src/app/pages/auth/components/login/login.component.html
Normal file
@ -0,0 +1,31 @@
|
||||
<div class="auth-container">
|
||||
<h2>Iniciar Sesión</h2>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Email</ion-label>
|
||||
<ion-input type="email" formControlName="email"></ion-input>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('required')">
|
||||
Email es requerido
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('email')">
|
||||
Formato de email inválido
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Contraseña</ion-label>
|
||||
<ion-input type="password" formControlName="password"></ion-input>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('required')">
|
||||
Contraseña es requerida
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('minlength')">
|
||||
La contraseña debe tener al menos 6 caracteres
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
||||
Iniciar Sesión
|
||||
</ion-button>
|
||||
</form>
|
||||
</div>
|
||||
17
src/app/pages/auth/components/login/login.component.scss
Normal file
17
src/app/pages/auth/components/login/login.component.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
ion-button {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
118
src/app/pages/auth/components/login/login.component.ts
Normal file
118
src/app/pages/auth/components/login/login.component.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { LoadingController, ToastController } from '@ionic/angular';
|
||||
import { AuthService } from '../../../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
@Output() loginSuccess = new EventEmitter<void>();
|
||||
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private loadingController: LoadingController,
|
||||
private toastController: ToastController
|
||||
) {
|
||||
this.loginForm = this.formBuilder.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
// Variable para controlar si hay un proceso de login en curso
|
||||
private isLoggingIn = false;
|
||||
|
||||
async onSubmit() {
|
||||
// Evitar múltiples clicks
|
||||
if (this.isLoggingIn) {
|
||||
console.log('Ya hay un proceso de login en curso, ignorando click');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.loginForm.invalid) {
|
||||
this.markFormGroupTouched(this.loginForm);
|
||||
return;
|
||||
}
|
||||
|
||||
// Marcar que hay un proceso en curso
|
||||
this.isLoggingIn = true;
|
||||
console.log('Iniciando proceso de login');
|
||||
|
||||
const loading = await this.loadingController.create({
|
||||
message: 'Iniciando sesión...',
|
||||
spinner: 'circles'
|
||||
});
|
||||
await loading.present();
|
||||
|
||||
// Intentar login con el backend real
|
||||
this.authService.login(this.loginForm.value).subscribe({
|
||||
next: () => {
|
||||
loading.dismiss();
|
||||
this.isLoggingIn = false; // Resetear flag
|
||||
this.loginSuccess.emit();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error en login:', error);
|
||||
loading.dismiss();
|
||||
this.isLoggingIn = false; // Resetear flag
|
||||
|
||||
// Si el backend rechaza, intentar con el usuario demo
|
||||
if (this.loginForm.value.email === 'usuario@ejemplo.com' &&
|
||||
this.loginForm.value.password === 'password123') {
|
||||
|
||||
// Simular login exitoso para el usuario demo
|
||||
this.isLoggingIn = true; // Volver a establecer el flag
|
||||
this.authService.simulateAuth({
|
||||
id: '1',
|
||||
name: 'Usuario Demo',
|
||||
email: 'usuario@ejemplo.com',
|
||||
profilePic: '',
|
||||
preferences: {
|
||||
notifications: true,
|
||||
favoriteClasses: []
|
||||
}
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.isLoggingIn = false;
|
||||
this.loginSuccess.emit();
|
||||
},
|
||||
error: () => {
|
||||
this.isLoggingIn = false;
|
||||
this.showToast('Error al iniciar sesión');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.showToast('Credenciales incorrectas');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private markFormGroupTouched(formGroup: FormGroup) {
|
||||
Object.values(formGroup.controls).forEach(control => {
|
||||
control.markAsTouched();
|
||||
if (control instanceof FormGroup) {
|
||||
this.markFormGroupTouched(control);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async showToast(message: string) {
|
||||
const toast = await this.toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: 'bottom',
|
||||
color: 'danger'
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<div class="auth-container">
|
||||
<h2>Crear Cuenta</h2>
|
||||
|
||||
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Nombre Completo</ion-label>
|
||||
<ion-input type="text" formControlName="name"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('name')?.touched && registerForm.get('name')?.hasError('required')">
|
||||
Nombre es requerido
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('name')?.touched && registerForm.get('name')?.hasError('minlength')">
|
||||
El nombre debe tener al menos 3 caracteres
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Email</ion-label>
|
||||
<ion-input type="email" formControlName="email"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('required')">
|
||||
Email es requerido
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('email')">
|
||||
Formato de email inválido
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Contraseña</ion-label>
|
||||
<ion-input type="password" formControlName="password"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('required')">
|
||||
Contraseña es requerida
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('minlength')">
|
||||
La contraseña debe tener al menos 6 caracteres
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label position="floating">Confirmar Contraseña</ion-label>
|
||||
<ion-input type="password" formControlName="confirmPassword"></ion-input>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('confirmPassword')?.touched && registerForm.get('confirmPassword')?.hasError('required')">
|
||||
Confirma tu contraseña
|
||||
</ion-note>
|
||||
<ion-note slot="error" *ngIf="registerForm.get('confirmPassword')?.touched && registerForm.get('confirmPassword')?.hasError('mismatch')">
|
||||
Las contraseñas no coinciden
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
||||
Crear Cuenta
|
||||
</ion-button>
|
||||
</form>
|
||||
</div>
|
||||
@ -0,0 +1,17 @@
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
ion-button {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
115
src/app/pages/auth/components/register/register.component.ts
Normal file
115
src/app/pages/auth/components/register/register.component.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { LoadingController, ToastController } from '@ionic/angular';
|
||||
import { AuthService } from '../../../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss'],
|
||||
standalone:false
|
||||
})
|
||||
export class RegisterComponent implements OnInit {
|
||||
@Output() registerSuccess = new EventEmitter<void>();
|
||||
|
||||
registerForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private loadingController: LoadingController,
|
||||
private toastController: ToastController
|
||||
) {
|
||||
this.registerForm = this.formBuilder.group({
|
||||
name: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
confirmPassword: ['', [Validators.required]]
|
||||
}, { validators: this.passwordMatchValidator });
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
passwordMatchValidator(form: FormGroup) {
|
||||
const password = form.get('password')?.value;
|
||||
const confirmPassword = form.get('confirmPassword')?.value;
|
||||
|
||||
if (password === confirmPassword) {
|
||||
form.get('confirmPassword')?.setErrors(null);
|
||||
return null;
|
||||
} else {
|
||||
form.get('confirmPassword')?.setErrors({ mismatch: true });
|
||||
return { mismatch: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Variable para controlar si hay un proceso de registro en curso
|
||||
private isRegistering = false;
|
||||
|
||||
async onSubmit() {
|
||||
// Evitar múltiples clicks
|
||||
if (this.isRegistering) {
|
||||
console.log('Ya hay un proceso de registro en curso, ignorando click');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registerForm.invalid) {
|
||||
this.markFormGroupTouched(this.registerForm);
|
||||
return;
|
||||
}
|
||||
|
||||
// Marcar que hay un proceso en curso
|
||||
this.isRegistering = true;
|
||||
console.log('Iniciando proceso de registro');
|
||||
|
||||
const loading = await this.loadingController.create({
|
||||
message: 'Creando cuenta...',
|
||||
spinner: 'circles'
|
||||
});
|
||||
await loading.present();
|
||||
|
||||
// Registrar el usuario en el servidor
|
||||
this.authService.register({
|
||||
name: this.registerForm.value.name,
|
||||
email: this.registerForm.value.email,
|
||||
password: this.registerForm.value.password
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
loading.dismiss();
|
||||
this.isRegistering = false; // Resetear flag
|
||||
this.registerSuccess.emit();
|
||||
this.showToast('Cuenta creada correctamente', 'success');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error en registro:', error);
|
||||
loading.dismiss();
|
||||
this.isRegistering = false; // Resetear flag
|
||||
|
||||
if (error?.error?.message === 'El usuario ya existe') {
|
||||
this.showToast('Este email ya está registrado');
|
||||
} else {
|
||||
this.showToast('Error al crear la cuenta');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private markFormGroupTouched(formGroup: FormGroup) {
|
||||
Object.values(formGroup.controls).forEach(control => {
|
||||
control.markAsTouched();
|
||||
if (control instanceof FormGroup) {
|
||||
this.markFormGroupTouched(control);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async showToast(message: string, color: string = 'danger') {
|
||||
const toast = await this.toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: 'bottom',
|
||||
color: color
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
}
|
||||
26
src/app/pages/auth/guards/auth.guard.ts
Normal file
26
src/app/pages/auth/guards/auth.guard.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
// Esperar a que se inicialice la sesión
|
||||
await this.authService.waitForInitialization();
|
||||
|
||||
if (this.authService.isLoggedIn()) {
|
||||
return true;
|
||||
} else {
|
||||
this.router.navigate(['/auth']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/tabs/classes"></ion-back-button>
|
||||
<ion-back-button defaultHref="/app/classes"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Detalle de Clase</ion-title>
|
||||
</ion-toolbar>
|
||||
|
||||
@ -36,7 +36,7 @@ export class ClassDetailPage implements OnInit {
|
||||
cargarDatosClase() {
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
if (!id) {
|
||||
this.navCtrl.navigateBack('/tabs/classes');
|
||||
this.navCtrl.navigateBack('/app/classes');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export class ClassDetailPage implements OnInit {
|
||||
if (gymClass) {
|
||||
this.gymClass = gymClass;
|
||||
} else {
|
||||
this.navCtrl.navigateBack('/tabs/classes');
|
||||
this.navCtrl.navigateBack('/app/classes');
|
||||
this.mostrarToast('Clase no encontrada', 'danger');
|
||||
}
|
||||
this.cargando = false;
|
||||
@ -54,7 +54,7 @@ export class ClassDetailPage implements OnInit {
|
||||
error: (error) => {
|
||||
console.error('Error al cargar clase', error);
|
||||
this.cargando = false;
|
||||
this.navCtrl.navigateBack('/tabs/classes');
|
||||
this.navCtrl.navigateBack('/app/classes');
|
||||
this.mostrarToast('Error al cargar la información', 'danger');
|
||||
}
|
||||
});
|
||||
@ -103,7 +103,7 @@ export class ClassDetailPage implements OnInit {
|
||||
{
|
||||
text: 'Ver Mis Reservas',
|
||||
handler: () => {
|
||||
this.navCtrl.navigateForward('/tabs/bookings');
|
||||
this.navCtrl.navigateForward('/app/bookings');
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
</div>
|
||||
|
||||
<ion-list *ngIf="!cargando">
|
||||
<ion-item *ngFor="let gymClass of clasesFiltradas" [routerLink]="['/tabs/classes', gymClass.id]" detail>
|
||||
<ion-item *ngFor="let gymClass of clasesFiltradas" [routerLink]="['/app/classes', gymClass.id]" detail>
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-img [src]="gymClass.imageUrl || 'https://cdn-icons-png.flaticon.com/512/140/140627.png'"></ion-img>
|
||||
</ion-thumbnail>
|
||||
|
||||
@ -338,20 +338,17 @@ export class ProfilePage implements OnInit {
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
cerrarSesion() {
|
||||
this.authService.logout().subscribe({
|
||||
next: () => {
|
||||
// En una app real, redirigir a la pantalla de login
|
||||
this.mostrarToast('Sesión cerrada', 'success');
|
||||
async cerrarSesion() {
|
||||
try {
|
||||
await this.authService.logout();
|
||||
this.mostrarToast('Sesión cerrada', 'success');
|
||||
|
||||
// Para este demo, simplemente reiniciamos a la primera pestaña
|
||||
this.navController.navigateRoot('/tabs/classes');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al cerrar sesión', error);
|
||||
this.mostrarToast('Error al cerrar sesión', 'danger');
|
||||
}
|
||||
});
|
||||
// Redirigir a la pantalla de login
|
||||
this.navController.navigateRoot('/auth');
|
||||
} catch (error) {
|
||||
console.error('Error al cerrar sesión', error);
|
||||
this.mostrarToast('Error al cerrar sesión', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async mostrarToast(mensaje: string, color: string = 'primary') {
|
||||
|
||||
@ -4,41 +4,189 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { User } from '../models/user.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
// Clave para almacenar información de sesión
|
||||
const AUTH_TOKEN_KEY = 'auth_token';
|
||||
const USER_KEY = 'user_data';
|
||||
|
||||
// Interfaz para credenciales de login
|
||||
interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Interfaz para datos de registro
|
||||
interface RegisterData {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = `${environment.apiUrl}/api/users`;
|
||||
private authUrl = `${environment.apiUrl}/api/auth`;
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
// Cargar usuario inicial (para demo)
|
||||
this.http.get<User>(`${this.apiUrl}/1`).subscribe(user => {
|
||||
// Asegurarnos de mantener compatibilidad entre backend y frontend
|
||||
if (user.profilePicUrl && !user.profilePic) {
|
||||
user.profilePic = user.profilePicUrl;
|
||||
} else if (user.profilePic && !user.profilePicUrl) {
|
||||
user.profilePicUrl = user.profilePic;
|
||||
}
|
||||
|
||||
// Si existe notificationsEnabled pero no preferences
|
||||
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
||||
user.preferences = {
|
||||
notifications: user.notificationsEnabled,
|
||||
favoriteClasses: []
|
||||
};
|
||||
}
|
||||
|
||||
this.currentUserSubject.next(user);
|
||||
});
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
// Intentar recuperar sesión anterior
|
||||
this.initializeSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa la sesión si hay datos guardados
|
||||
*/
|
||||
async initializeSession() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
const userData = await this.storageService.get(USER_KEY) as User | null;
|
||||
|
||||
if (userData) {
|
||||
console.log('Sesión recuperada del almacenamiento local:', userData);
|
||||
// Normalizar campos para compatibilidad
|
||||
this.normalizeUserData(userData);
|
||||
this.currentUserSubject.next(userData);
|
||||
} else {
|
||||
console.log('No hay sesión guardada, redirigiendo a login');
|
||||
this.currentUserSubject.next(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al inicializar sesión:', error);
|
||||
this.currentUserSubject.next(null);
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Espera a que se complete la inicialización
|
||||
* Importante para evitar redirecciones erróneas cuando la app inicia
|
||||
*/
|
||||
async waitForInitialization(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
// Si no se ha inicializado, iniciamos el proceso
|
||||
await this.initializeSession();
|
||||
|
||||
// Esperar hasta que se inicialice (con timeout)
|
||||
let attempts = 0;
|
||||
while (!this.isInitialized && attempts < 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza los datos de usuario para compatibilidad entre frontend/backend
|
||||
*/
|
||||
private normalizeUserData(user: User) {
|
||||
// Sincronizar campos de imagen
|
||||
if (user.profilePicUrl && !user.profilePic) {
|
||||
user.profilePic = user.profilePicUrl;
|
||||
} else if (user.profilePic && !user.profilePicUrl) {
|
||||
user.profilePicUrl = user.profilePic;
|
||||
}
|
||||
|
||||
// Sincronizar preferencias de notificaciones
|
||||
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
||||
user.preferences = {
|
||||
notifications: user.notificationsEnabled,
|
||||
favoriteClasses: []
|
||||
};
|
||||
} else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
|
||||
user.notificationsEnabled = user.preferences.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
return this.currentUserSubject.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iniciar sesión con email y contraseña
|
||||
*/
|
||||
login(credentials: LoginCredentials): Observable<User> {
|
||||
console.log('Intentando login con:', credentials);
|
||||
return this.http.post<any>(`${this.authUrl}/login`, credentials).pipe(
|
||||
tap(async (response) => {
|
||||
console.log('Respuesta de login:', response);
|
||||
|
||||
// Guardar token si el backend lo devuelve
|
||||
if (response.token) {
|
||||
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
||||
}
|
||||
|
||||
// Verificar y normalizar datos del usuario
|
||||
const user = response.user || response;
|
||||
this.normalizeUserData(user);
|
||||
|
||||
// Guardar datos del usuario
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
// Actualizar estado de sesión
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
map(response => response.user || response)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar un nuevo usuario
|
||||
*/
|
||||
register(userData: RegisterData): Observable<User> {
|
||||
console.log('Registrando usuario:', userData);
|
||||
const registerData = {
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
return this.http.post<any>(`${this.authUrl}/register`, registerData).pipe(
|
||||
tap(async (response) => {
|
||||
console.log('Respuesta de registro:', response);
|
||||
|
||||
// Guardar token si el backend lo devuelve
|
||||
if (response.token) {
|
||||
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
||||
}
|
||||
|
||||
// Verificar y normalizar datos del usuario
|
||||
const user = response.user || response;
|
||||
this.normalizeUserData(user);
|
||||
|
||||
// Guardar datos del usuario
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
// Actualizar estado de sesión
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
map(response => response.user || response)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Método para simular login/registro en una app de demo
|
||||
*/
|
||||
simulateAuth(userData: User): Observable<User> {
|
||||
return of(userData).pipe(
|
||||
tap(async (user) => {
|
||||
this.normalizeUserData(user);
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
this.currentUserSubject.next(user);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateUserProfile(userData: Partial<User>): Observable<User> {
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
@ -72,7 +220,7 @@ export class AuthService {
|
||||
};
|
||||
|
||||
return this.http.put<User>(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
|
||||
tap(user => {
|
||||
tap(async (user) => {
|
||||
console.log('Usuario actualizado correctamente:', user);
|
||||
|
||||
// Sincronizar campos para mantener compatibilidad
|
||||
@ -90,6 +238,9 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
// Actualizar almacenamiento local
|
||||
await this.storageService.set(USER_KEY, user);
|
||||
|
||||
this.currentUserSubject.next(user);
|
||||
}),
|
||||
catchError(error => {
|
||||
@ -103,9 +254,32 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
logout(): Observable<any> {
|
||||
// En una aplicación real, aquí se cerraría la sesión en el backend
|
||||
this.currentUserSubject.next(null);
|
||||
return of(true);
|
||||
/**
|
||||
* Cerrar sesión y eliminar datos
|
||||
*/
|
||||
async logout(): Promise<boolean> {
|
||||
try {
|
||||
// En una aplicación real, enviaríamos petición al backend
|
||||
// this.http.post(`${this.authUrl}/logout`, {}).subscribe();
|
||||
|
||||
// Limpiar datos de sesión
|
||||
await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
|
||||
|
||||
// Actualizar estado
|
||||
this.currentUserSubject.next(null);
|
||||
this.isInitialized = false; // Permitir nueva inicialización
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error al cerrar sesión:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si hay sesión activa
|
||||
*/
|
||||
isLoggedIn(): boolean {
|
||||
return !!this.currentUserSubject.value;
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { Booking } from '../models/booking.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
// URL base para las imágenes
|
||||
const BASE_IMAGE_URL = environment.apiUrl;
|
||||
@ -12,13 +13,25 @@ const BASE_IMAGE_URL = environment.apiUrl;
|
||||
})
|
||||
export class BookingsService {
|
||||
private apiUrl = `${environment.apiUrl}/api/bookings`;
|
||||
private userId = '1'
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private authService: AuthService
|
||||
) { }
|
||||
|
||||
getUserBookings(): Observable<Booking[]> {
|
||||
console.log('Obteniendo reservas del usuario...', `${this.apiUrl}/user/${this.userId}`);
|
||||
return this.http.get<any[]>(`${this.apiUrl}/user/${this.userId}`).pipe(
|
||||
const currentUser = this.authService.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
console.warn('No hay usuario autenticado para obtener reservas');
|
||||
return new Observable(observer => {
|
||||
observer.next([]);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
const userId = currentUser.id;
|
||||
console.log('Obteniendo reservas del usuario...', `${this.apiUrl}/user/${userId}`);
|
||||
return this.http.get<any[]>(`${this.apiUrl}/user/${userId}`).pipe(
|
||||
map(bookings => {
|
||||
return bookings.map(booking => {
|
||||
// El backend devuelve booking.gymclass (minúscula) pero nuestro modelo usa gymClass (capitalizada)
|
||||
@ -47,9 +60,18 @@ export class BookingsService {
|
||||
}
|
||||
|
||||
addBooking(classId: string): Observable<Booking> {
|
||||
const currentUser = this.authService.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
console.error('No hay usuario autenticado para crear reserva');
|
||||
return new Observable(observer => {
|
||||
observer.error(new Error('No hay usuario autenticado'));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Agregando reserva...', this.apiUrl);
|
||||
return this.http.post<any>(this.apiUrl, {
|
||||
userId: this.userId,
|
||||
userId: currentUser.id,
|
||||
gymClassId: classId
|
||||
}).pipe(
|
||||
map(booking => {
|
||||
|
||||
@ -4,27 +4,82 @@ import { Preferences } from '@capacitor/preferences';
|
||||
// Clave para almacenar la imagen de perfil en el almacenamiento local
|
||||
const PROFILE_IMAGE_KEY = 'profileImage';
|
||||
|
||||
// Flag para evitar inicializaciones múltiples
|
||||
let __storageInitialized = false;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
|
||||
constructor() { }
|
||||
constructor() {
|
||||
this.initializeStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el almacenamiento y verifica que funcione correctamente
|
||||
*/
|
||||
private async initializeStorage() {
|
||||
if (__storageInitialized) return;
|
||||
|
||||
try {
|
||||
// Guardar un valor de prueba para verificar que el almacenamiento funciona
|
||||
await Preferences.set({
|
||||
key: '__storage_test',
|
||||
value: 'ok'
|
||||
});
|
||||
|
||||
// Verificar que podemos recuperar el valor
|
||||
const { value } = await Preferences.get({ key: '__storage_test' });
|
||||
|
||||
if (value === 'ok') {
|
||||
console.log('Almacenamiento local inicializado correctamente');
|
||||
__storageInitialized = true;
|
||||
} else {
|
||||
console.error('Error al inicializar el almacenamiento: valor de prueba incorrecto');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al inicializar el almacenamiento:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: any): Promise<void> {
|
||||
await this.waitForInitialization();
|
||||
|
||||
try {
|
||||
// Manejar blobs y data URIs en objetos complejos
|
||||
const processedValue = this.prepareValueForStorage(value);
|
||||
|
||||
// Convertir a string y almacenar
|
||||
const valueStr = JSON.stringify(processedValue);
|
||||
console.log(`Guardando en storage (${key}):`, valueStr.length > 100 ? valueStr.substring(0, 100) + '...' : valueStr);
|
||||
|
||||
await Preferences.set({
|
||||
key,
|
||||
value: JSON.stringify(processedValue)
|
||||
value: valueStr
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error al guardar en storage (${key}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Espera a que se inicialice el almacenamiento
|
||||
*/
|
||||
private async waitForInitialization() {
|
||||
if (__storageInitialized) return;
|
||||
|
||||
let attempts = 0;
|
||||
while (!__storageInitialized && attempts < 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!__storageInitialized) {
|
||||
await this.initializeStorage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepara valores para almacenamiento, manejando casos especiales
|
||||
* como objetos con Blobs que no se pueden serializar
|
||||
@ -68,21 +123,43 @@ export class StorageService {
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
const { value } = await Preferences.get({ key });
|
||||
await this.waitForInitialization();
|
||||
|
||||
if (value) {
|
||||
return JSON.parse(value);
|
||||
try {
|
||||
const { value } = await Preferences.get({ key });
|
||||
|
||||
if (value) {
|
||||
const parsed = JSON.parse(value);
|
||||
console.log(`Recuperando de storage (${key}):`,
|
||||
typeof parsed === 'object' ? 'Objeto recuperado' :
|
||||
(value.length > 100 ? value.substring(0, 100) + '...' : value));
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error al recuperar de storage (${key}):`, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
await Preferences.remove({ key });
|
||||
await this.waitForInitialization();
|
||||
try {
|
||||
await Preferences.remove({ key });
|
||||
console.log(`Eliminado del storage: ${key}`);
|
||||
} catch (error) {
|
||||
console.error(`Error al eliminar de storage (${key}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await Preferences.clear();
|
||||
await this.waitForInitialization();
|
||||
try {
|
||||
await Preferences.clear();
|
||||
console.log('Storage limpiado completamente');
|
||||
} catch (error) {
|
||||
console.error('Error al limpiar storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,13 +4,17 @@ import { TabsPage } from './tabs.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'tabs',
|
||||
path: '',
|
||||
component: TabsPage,
|
||||
children: [
|
||||
{
|
||||
path: 'classes',
|
||||
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
|
||||
},
|
||||
{
|
||||
path: 'classes/:id',
|
||||
loadChildren: () => import('../pages/class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
|
||||
},
|
||||
{
|
||||
path: 'bookings',
|
||||
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
||||
@ -21,15 +25,10 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/tabs/classes',
|
||||
redirectTo: '/app/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/tabs/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl: 'https://taller-ionic-backend-production.up.railway.app'
|
||||
apiUrl: 'https://taller.ionic.lcespedes.dev'
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://taller-ionic-backend-production.up.railway.app'
|
||||
apiUrl: 'https://taller.ionic.lcespedes.dev'
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user