This commit is contained in:
luis cespedes 2025-04-24 15:57:53 -04:00
parent 35f04423c4
commit e67e1abdfb
32 changed files with 10290 additions and 67 deletions

View 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
});
}
};

View 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;

View File

@ -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
View 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
View 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
View 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
View 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)
![enter image description here](https://i.ibb.co/Q3mcpwKt/imagen.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
![enter image description here](https://i.ibb.co/VWc1Nfx9/imagen.png)
![proyecto,corriendo](https://i.ibb.co/gLfFxtVv/imagen.png)
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&#64;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.

1495
cap5.md Normal file

File diff suppressed because it is too large Load Diff

711
cap6.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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',

View 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 {}

View 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&#64;ejemplo.com</p>
<p><strong>Contraseña:</strong> password123</p>
</div>
</ion-content>

View 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);
}
}

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

View 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>

View 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;
}

View 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();
}
}

View File

@ -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>

View 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;
}

View 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();
}
}

View 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;
}
}
}

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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');
// 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');
}
});
async cerrarSesion() {
try {
await this.authService.logout();
this.mostrarToast('Sesión cerrada', 'success');
// 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') {

View File

@ -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;
}
}

View File

@ -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 => {

View File

@ -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);
}
}
/**

View File

@ -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'
}
];

View File

@ -1,4 +1,4 @@
export const environment = {
production: true,
apiUrl: 'https://taller-ionic-backend-production.up.railway.app'
apiUrl: 'https://taller.ionic.lcespedes.dev'
};

View File

@ -4,7 +4,7 @@
export const environment = {
production: false,
apiUrl: 'https://taller-ionic-backend-production.up.railway.app'
apiUrl: 'https://taller.ionic.lcespedes.dev'
};
/*