941 lines
28 KiB
Markdown
941 lines
28 KiB
Markdown
# Capítulo 4: Desarrollo Práctico de la Aplicación de Reservas
|
|
|
|
En este capítulo comenzaremos el desarrollo práctico de nuestra aplicación de reservas para gimnasio utilizando Ionic y Angular. Seguiremos paso a paso la implementación desde cero.
|
|
|
|
## 1. Creación y Configuración del Proyecto
|
|
|
|
### Crear el proyecto
|
|
|
|
Lo primero es crear el proyecto mediante el CLI de Ionic:
|
|
|
|
> Es necesario recordar de que se tiene que tener node instalado, en este caso yo personalmente estoy usando la versión LTS de node (22.14)
|
|
|
|
```bash
|
|
# Asegúrate de tener instalado Ionic CLI y Angular CLI
|
|
npm install -g @ionic/cli @angular/cli
|
|
|
|
# Crear el proyecto con el template de tabs
|
|
ionic start taller tabs --type=angular
|
|
|
|
# Entrar al directorio del proyecto
|
|
cd taller
|
|
```
|
|
|
|
![\[img\]https://i.ibb.co/Xx1kxbMn/instalar-cli.png\[/img\]](https://i.ibb.co/Xx1kxbMn/instalar-cli.png)
|
|

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

|
|
|
|

|
|
Deberías ver la aplicación por defecto en tu navegador, con tres tabs básicos.
|
|
|
|
## 2. Organización del Proyecto
|
|
|
|
Vamos a reorganizar la estructura para adaptarla a nuestras necesidades:
|
|
|
|
```bash
|
|
# Crear directorios para nuestra estructura, puedes crearlos manualmente,
|
|
# o en mi caso que estoy usando una distribución linux los haré con estos comandos
|
|
mkdir -p src/app/pages
|
|
mkdir -p src/app/services
|
|
mkdir -p src/app/models
|
|
mkdir -p src/app/shared/components
|
|
```
|
|
|
|
## 3. Definición de Modelos
|
|
|
|
Creemos los modelos de datos que necesitaremos:
|
|
|
|
```bash
|
|
# Crear archivos para los modelos
|
|
# lo mismo de arriba, puedes crearlos manualmente, en mi caso usaré estos comandos
|
|
touch src/app/models/gym-class.model.ts
|
|
touch src/app/models/booking.model.ts
|
|
touch src/app/models/user.model.ts
|
|
```
|
|
|
|
Ahora implementemos las interfaces para cada modelo:
|
|
|
|
**gym-class.model.ts**:
|
|
|
|
```typescript
|
|
export interface GymClass {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
instructor: string;
|
|
startTime: Date;
|
|
endTime: Date;
|
|
maxCapacity: number;
|
|
currentBookings: number;
|
|
category?: string;
|
|
imageUrl?: string;
|
|
}
|
|
|
|
```
|
|
|
|
**booking.model.ts**:
|
|
|
|
```typescript
|
|
export interface Booking {
|
|
id: string;
|
|
userId: string;
|
|
classId: string;
|
|
className: string;
|
|
date: Date;
|
|
status: 'confirmed' | 'cancelled' | 'pending';
|
|
}
|
|
|
|
```
|
|
|
|
**user.model.ts**:
|
|
|
|
```typescript
|
|
export interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
profilePic?: string;
|
|
profilePicUrl?: string; // Para compatibilidad con el backend
|
|
notificationsEnabled?: boolean; // Para compatibilidad con el backend
|
|
preferences?: {
|
|
notifications: boolean;
|
|
favoriteClasses?: string[];
|
|
};
|
|
}
|
|
```
|
|
|
|
## 4. Creación de Servicios
|
|
|
|
Ahora, implementaremos los servicios que gestionarán los datos:
|
|
|
|
```bash
|
|
# Generar servicios usando el CLI de Ionic
|
|
ionic generate service services/classes
|
|
ionic generate service services/bookings
|
|
ionic generate service services/auth
|
|
ionic generate service services/storage
|
|
ionic generate service services/upload
|
|
ionic generate service services/notification
|
|
```
|
|
|
|
### Implementación del Servicio de Almacenamiento
|
|
|
|
Primero vamos a instalar el plugin de Preferences de Capacitor:
|
|
|
|
> Este plugin es básicamente para guardar datos o caché de nuestra app, en caso de que la exportemos para móviles como Android o iOS
|
|
|
|
```bash
|
|
npm install @capacitor/preferences
|
|
```
|
|
|
|
Ahora implementemos el servicio de almacenamiento:
|
|
|
|
**services/storage.service.ts**:
|
|
|
|
```typescript
|
|
import { Injectable } from '@angular/core';
|
|
import { Preferences } from '@capacitor/preferences';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class StorageService {
|
|
|
|
constructor() { }
|
|
|
|
async set(key: string, value: any): Promise<void> {
|
|
await Preferences.set({
|
|
key,
|
|
value: JSON.stringify(value)
|
|
});
|
|
}
|
|
|
|
async get(key: string): Promise<any> {
|
|
const { value } = await Preferences.get({ key });
|
|
|
|
if (value) {
|
|
return JSON.parse(value);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async remove(key: string): Promise<void> {
|
|
await Preferences.remove({ key });
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
await Preferences.clear();
|
|
}
|
|
}
|
|
```
|
|
|
|
## 5. Configuración de Entornos
|
|
|
|
Configuremos los archivos de entorno para facilitar el cambio entre desarrollo y producción:
|
|
|
|
**src/environments/environment.ts**:
|
|
|
|
```typescript
|
|
export const environment = {
|
|
production: false,
|
|
apiUrl: 'http://localhost:3000' // URL de la API de desarrollo
|
|
};
|
|
```
|
|
|
|
**src/environments/environment.prod.ts**:
|
|
|
|
```typescript
|
|
export const environment = {
|
|
production: true,
|
|
apiUrl: 'https://taller.ionic.lcespedes.dev' // URL de la API de producción
|
|
};
|
|
```
|
|
|
|
## 6. Integración con Backend
|
|
|
|
Nuestra aplicación se conectará a un backend real en lugar de utilizar datos mockeados. Instalemos las dependencias necesarias:
|
|
|
|
```bash
|
|
npm install @angular/common/http
|
|
```
|
|
|
|
### Configuración del Módulo HTTP
|
|
|
|
Agreguemos el módulo HTTP al app.module.ts:
|
|
|
|
```typescript
|
|
import { NgModule } from '@angular/core';
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { RouteReuseStrategy } from '@angular/router';
|
|
import { HttpClientModule } from '@angular/common/http';
|
|
|
|
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
|
|
|
import { AppRoutingModule } from './app-routing.module';
|
|
import { AppComponent } from './app.component';
|
|
|
|
@NgModule({
|
|
declarations: [AppComponent],
|
|
imports: [
|
|
BrowserModule,
|
|
IonicModule.forRoot(),
|
|
AppRoutingModule,
|
|
HttpClientModule
|
|
],
|
|
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
|
|
bootstrap: [AppComponent],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
## 7. Implementación de Autenticación
|
|
|
|
Crearemos un módulo de autenticación completo:
|
|
|
|
```bash
|
|
ionic generate page pages/auth
|
|
ionic generate component pages/auth/components/login
|
|
ionic generate component pages/auth/components/register
|
|
ionic generate guard pages/auth/guards/auth
|
|
```
|
|
|
|
### Implementación del Guard de Autenticación
|
|
|
|
**src/app/pages/auth/guards/auth.guard.ts**:
|
|
|
|
```typescript
|
|
import { Injectable } from '@angular/core';
|
|
import { CanActivate, Router } from '@angular/router';
|
|
import { AuthService } from '../../../services/auth.service';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class AuthGuard implements CanActivate {
|
|
|
|
constructor(
|
|
private authService: AuthService,
|
|
private router: Router
|
|
) {}
|
|
|
|
async canActivate(): Promise<boolean> {
|
|
// Esperar a que el servicio de autenticación esté inicializado
|
|
await this.authService.waitForInitialization();
|
|
|
|
if (this.authService.isLoggedIn()) {
|
|
return true;
|
|
}
|
|
|
|
// Redirigir al login si no hay sesión
|
|
this.router.navigateByUrl('/auth');
|
|
return false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuración de Rutas Principales
|
|
|
|
Actualizaremos el app-routing.module.ts para implementar las rutas correctas:
|
|
|
|
```typescript
|
|
import { NgModule } from '@angular/core';
|
|
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
|
import { AuthGuard } from './pages/auth/guards/auth.guard';
|
|
|
|
const routes: Routes = [
|
|
{
|
|
path: '',
|
|
redirectTo: 'auth',
|
|
pathMatch: 'full'
|
|
},
|
|
{
|
|
path: 'app',
|
|
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule),
|
|
canActivate: [AuthGuard]
|
|
},
|
|
{
|
|
path: 'auth',
|
|
loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
|
|
},
|
|
{
|
|
path: 'classes',
|
|
loadChildren: () => import('./pages/classes/classes.module').then(m => m.ClassesPageModule)
|
|
},
|
|
{
|
|
path: 'class-detail',
|
|
loadChildren: () => import('./pages/class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
|
|
},
|
|
{
|
|
path: 'bookings',
|
|
loadChildren: () => import('./pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
|
},
|
|
{
|
|
path: 'profile',
|
|
loadChildren: () => import('./pages/profile/profile.module').then(m => m.ProfilePageModule)
|
|
}
|
|
];
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
|
|
],
|
|
exports: [RouterModule]
|
|
})
|
|
export class AppRoutingModule {}
|
|
```
|
|
|
|
## 8. Creación de Páginas Principales
|
|
|
|
Vamos a crear las páginas principales de nuestra aplicación:
|
|
|
|
```bash
|
|
# Eliminar las páginas de tabs generadas automáticamente
|
|
rm -rf src/app/tab1
|
|
rm -rf src/app/tab2
|
|
rm -rf src/app/tab3
|
|
|
|
# Generar nuestras propias páginas
|
|
ionic generate page pages/classes
|
|
ionic generate page pages/class-detail
|
|
ionic generate page pages/bookings
|
|
ionic generate page pages/profile
|
|
```
|
|
|
|
### Configuración de las rutas y tabs
|
|
|
|
Actualizamos el archivo de rutas de tabs:
|
|
|
|
**src/app/tabs/tabs-routing.module.ts**:
|
|
|
|
```typescript
|
|
import { NgModule } from '@angular/core';
|
|
import { RouterModule, Routes } from '@angular/router';
|
|
import { TabsPage } from './tabs.page';
|
|
|
|
const routes: Routes = [
|
|
{
|
|
path: '',
|
|
component: TabsPage,
|
|
children: [
|
|
{
|
|
path: 'classes',
|
|
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
|
|
},
|
|
{
|
|
path: 'bookings',
|
|
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
|
},
|
|
{
|
|
path: 'profile',
|
|
loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
|
|
},
|
|
{
|
|
path: '',
|
|
redirectTo: '/app/classes',
|
|
pathMatch: 'full'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
path: '',
|
|
redirectTo: '/app/classes',
|
|
pathMatch: 'full'
|
|
}
|
|
];
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild(routes)],
|
|
})
|
|
export class TabsPageRoutingModule {}
|
|
```
|
|
|
|
Actualizamos el template de tabs:
|
|
|
|
**src/app/tabs/tabs.page.html**:
|
|
|
|
```html
|
|
<ion-tabs>
|
|
<ion-tab-bar slot="bottom">
|
|
<ion-tab-button tab="classes">
|
|
<ion-icon name="calendar"></ion-icon>
|
|
<ion-label>Clases</ion-label>
|
|
</ion-tab-button>
|
|
|
|
<ion-tab-button tab="bookings">
|
|
<ion-icon name="bookmark"></ion-icon>
|
|
<ion-label>Mis Reservas</ion-label>
|
|
</ion-tab-button>
|
|
|
|
<ion-tab-button tab="profile">
|
|
<ion-icon name="person"></ion-icon>
|
|
<ion-label>Perfil</ion-label>
|
|
</ion-tab-button>
|
|
</ion-tab-bar>
|
|
</ion-tabs>
|
|
```
|
|
|
|
## 9. Implementar Servicio de Autenticación
|
|
|
|
Ahora implementaremos el servicio de autenticación real que se comunica con el backend:
|
|
|
|
**services/auth.service.ts**:
|
|
|
|
```typescript
|
|
import { Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { BehaviorSubject, Observable, of } from 'rxjs';
|
|
import { catchError, map, tap } from 'rxjs/operators';
|
|
import { User } from '../models/user.model';
|
|
import { environment } from '../../environments/environment';
|
|
import { StorageService } from './storage.service';
|
|
|
|
// Clave para almacenar información de sesión
|
|
const AUTH_TOKEN_KEY = 'auth_token';
|
|
const USER_KEY = 'user_data';
|
|
|
|
// Interfaz para credenciales de login
|
|
interface LoginCredentials {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
// Interfaz para datos de registro
|
|
interface RegisterData {
|
|
name: string;
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class AuthService {
|
|
private apiUrl = `${environment.apiUrl}/api/users`;
|
|
private authUrl = `${environment.apiUrl}/api/auth`;
|
|
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
|
public currentUser$ = this.currentUserSubject.asObservable();
|
|
private isInitialized = false;
|
|
|
|
constructor(
|
|
private http: HttpClient,
|
|
private storageService: StorageService
|
|
) {
|
|
// Intentar recuperar sesión anterior
|
|
this.initializeSession();
|
|
}
|
|
|
|
/**
|
|
* Inicializa la sesión si hay datos guardados
|
|
*/
|
|
async initializeSession() {
|
|
if (this.isInitialized) return;
|
|
|
|
try {
|
|
const userData = await this.storageService.get(USER_KEY) as User | null;
|
|
|
|
if (userData) {
|
|
console.log('Sesión recuperada del almacenamiento local:', userData);
|
|
// Normalizar campos para compatibilidad
|
|
this.normalizeUserData(userData);
|
|
this.currentUserSubject.next(userData);
|
|
} else {
|
|
console.log('No hay sesión guardada, redirigiendo a login');
|
|
this.currentUserSubject.next(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al inicializar sesión:', error);
|
|
this.currentUserSubject.next(null);
|
|
}
|
|
|
|
this.isInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* Espera a que se complete la inicialización
|
|
* Importante para evitar redirecciones erróneas cuando la app inicia
|
|
*/
|
|
async waitForInitialization(): Promise<void> {
|
|
if (this.isInitialized) return;
|
|
|
|
// Si no se ha inicializado, iniciamos el proceso
|
|
await this.initializeSession();
|
|
|
|
// Esperar hasta que se inicialice (con timeout)
|
|
let attempts = 0;
|
|
while (!this.isInitialized && attempts < 10) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
attempts++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normaliza los datos de usuario para compatibilidad entre frontend/backend
|
|
*/
|
|
private normalizeUserData(user: User) {
|
|
// Sincronizar campos de imagen
|
|
if (user.profilePicUrl && !user.profilePic) {
|
|
user.profilePic = user.profilePicUrl;
|
|
} else if (user.profilePic && !user.profilePicUrl) {
|
|
user.profilePicUrl = user.profilePic;
|
|
}
|
|
|
|
// Sincronizar preferencias de notificaciones
|
|
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
|
user.preferences = {
|
|
notifications: user.notificationsEnabled,
|
|
favoriteClasses: []
|
|
};
|
|
} else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
|
|
user.notificationsEnabled = user.preferences.notifications;
|
|
}
|
|
}
|
|
|
|
getCurrentUser(): User | null {
|
|
return this.currentUserSubject.value;
|
|
}
|
|
|
|
/**
|
|
* Iniciar sesión con email y contraseña
|
|
*/
|
|
login(credentials: LoginCredentials): Observable<User> {
|
|
console.log('Intentando login con:', credentials);
|
|
return this.http.post<any>(`${this.authUrl}/login`, credentials).pipe(
|
|
tap(async (response) => {
|
|
console.log('Respuesta de login:', response);
|
|
|
|
// Guardar token si el backend lo devuelve
|
|
if (response.token) {
|
|
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
|
}
|
|
|
|
// Verificar y normalizar datos del usuario
|
|
const user = response.user || response;
|
|
this.normalizeUserData(user);
|
|
|
|
// Guardar datos del usuario
|
|
await this.storageService.set(USER_KEY, user);
|
|
|
|
// Actualizar estado de sesión
|
|
this.currentUserSubject.next(user);
|
|
}),
|
|
map(response => response.user || response)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registrar un nuevo usuario
|
|
*/
|
|
register(userData: RegisterData): Observable<User> {
|
|
console.log('Registrando usuario:', userData);
|
|
const registerData = {
|
|
name: userData.name,
|
|
email: userData.email,
|
|
password: userData.password
|
|
};
|
|
|
|
return this.http.post<any>(`${this.authUrl}/register`, registerData).pipe(
|
|
tap(async (response) => {
|
|
console.log('Respuesta de registro:', response);
|
|
|
|
// Guardar token si el backend lo devuelve
|
|
if (response.token) {
|
|
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
|
|
}
|
|
|
|
// Verificar y normalizar datos del usuario
|
|
const user = response.user || response;
|
|
this.normalizeUserData(user);
|
|
|
|
// Guardar datos del usuario
|
|
await this.storageService.set(USER_KEY, user);
|
|
|
|
// Actualizar estado de sesión
|
|
this.currentUserSubject.next(user);
|
|
}),
|
|
map(response => response.user || response)
|
|
);
|
|
}
|
|
|
|
updateUserProfile(userData: Partial<User>): Observable<User> {
|
|
const currentUser = this.getCurrentUser();
|
|
if (!currentUser) {
|
|
return of(currentUser as unknown as User);
|
|
}
|
|
|
|
console.log('Actualizando perfil de usuario con:', userData);
|
|
|
|
// Asegurar que ambos campos de imagen estén sincronizados
|
|
if (userData.profilePic && !userData.profilePicUrl) {
|
|
userData.profilePicUrl = userData.profilePic;
|
|
} else if (userData.profilePicUrl && !userData.profilePic) {
|
|
userData.profilePic = userData.profilePicUrl;
|
|
}
|
|
|
|
// Sincronizar notificaciones entre los dos formatos
|
|
if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) {
|
|
userData.notificationsEnabled = userData.preferences.notifications;
|
|
} else if (userData.notificationsEnabled !== undefined &&
|
|
(!userData.preferences || userData.preferences.notifications === undefined)) {
|
|
if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] };
|
|
userData.preferences.notifications = userData.notificationsEnabled;
|
|
}
|
|
|
|
// Solo enviamos al servidor los campos que espera
|
|
const backendUserData = {
|
|
name: userData.name,
|
|
profilePicUrl: userData.profilePicUrl,
|
|
notificationsEnabled: userData.notificationsEnabled ||
|
|
(userData.preferences ? userData.preferences.notifications : undefined)
|
|
};
|
|
|
|
return this.http.put<User>(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
|
|
tap(async (user) => {
|
|
console.log('Usuario actualizado correctamente:', user);
|
|
|
|
// Sincronizar campos para mantener compatibilidad
|
|
if (user.profilePicUrl && !user.profilePic) {
|
|
user.profilePic = user.profilePicUrl;
|
|
} else if (user.profilePic && !user.profilePicUrl) {
|
|
user.profilePicUrl = user.profilePic;
|
|
}
|
|
|
|
// Mantener los campos que espera el frontend
|
|
if (user.notificationsEnabled !== undefined && !user.preferences) {
|
|
user.preferences = {
|
|
notifications: user.notificationsEnabled,
|
|
favoriteClasses: currentUser.preferences?.favoriteClasses || []
|
|
};
|
|
}
|
|
|
|
// Actualizar almacenamiento local
|
|
await this.storageService.set(USER_KEY, user);
|
|
|
|
this.currentUserSubject.next(user);
|
|
}),
|
|
catchError(error => {
|
|
console.error('Error al actualizar usuario:', error);
|
|
// Devolver el usuario actualizado localmente para que la UI no se rompa
|
|
// en caso de error de red
|
|
const updatedUser = { ...currentUser, ...userData };
|
|
this.currentUserSubject.next(updatedUser);
|
|
return of(updatedUser);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cerrar sesión y eliminar datos
|
|
*/
|
|
async logout(): Promise<boolean> {
|
|
try {
|
|
// En una aplicación real, enviaríamos petición al backend
|
|
// this.http.post(`${this.authUrl}/logout`, {}).subscribe();
|
|
|
|
// Limpiar datos de sesión
|
|
await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
|
|
|
|
// Actualizar estado
|
|
this.currentUserSubject.next(null);
|
|
this.isInitialized = false; // Permitir nueva inicialización
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error al cerrar sesión:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verificar si hay sesión activa
|
|
*/
|
|
isLoggedIn(): boolean {
|
|
return !!this.currentUserSubject.value;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 10. Creación de Componentes de Login y Registro
|
|
|
|
Implementemos los componentes de autenticación:
|
|
|
|
**pages/auth/components/login/login.component.html**:
|
|
|
|
```html
|
|
<div class="auth-container">
|
|
<h2>Iniciar Sesión</h2>
|
|
|
|
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
|
<ion-item class="ion-margin-bottom">
|
|
<ion-label position="floating">Email</ion-label>
|
|
<ion-input type="email" formControlName="email"></ion-input>
|
|
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('required')">
|
|
Email es requerido
|
|
</ion-note>
|
|
<ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('email')">
|
|
Formato de email inválido
|
|
</ion-note>
|
|
</ion-item>
|
|
|
|
<ion-item class="ion-margin-bottom">
|
|
<ion-label position="floating">Contraseña</ion-label>
|
|
<ion-input type="password" formControlName="password"></ion-input>
|
|
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('required')">
|
|
Contraseña es requerida
|
|
</ion-note>
|
|
<ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('minlength')">
|
|
La contraseña debe tener al menos 6 caracteres
|
|
</ion-note>
|
|
</ion-item>
|
|
|
|
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
|
Iniciar Sesión
|
|
</ion-button>
|
|
</form>
|
|
</div>
|
|
```
|
|
|
|
**pages/auth/components/register/register.component.html**:
|
|
|
|
```html
|
|
<div class="auth-container">
|
|
<h2>Crear Cuenta</h2>
|
|
|
|
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
|
|
<ion-item class="ion-margin-bottom">
|
|
<ion-label position="floating">Nombre</ion-label>
|
|
<ion-input type="text" formControlName="name"></ion-input>
|
|
<ion-note slot="error" *ngIf="registerForm.get('name')?.touched && registerForm.get('name')?.hasError('required')">
|
|
Nombre es requerido
|
|
</ion-note>
|
|
</ion-item>
|
|
|
|
<ion-item class="ion-margin-bottom">
|
|
<ion-label position="floating">Email</ion-label>
|
|
<ion-input type="email" formControlName="email"></ion-input>
|
|
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('required')">
|
|
Email es requerido
|
|
</ion-note>
|
|
<ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('email')">
|
|
Formato de email inválido
|
|
</ion-note>
|
|
</ion-item>
|
|
|
|
<ion-item class="ion-margin-bottom">
|
|
<ion-label position="floating">Contraseña</ion-label>
|
|
<ion-input type="password" formControlName="password"></ion-input>
|
|
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('required')">
|
|
Contraseña es requerida
|
|
</ion-note>
|
|
<ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('minlength')">
|
|
La contraseña debe tener al menos 6 caracteres
|
|
</ion-note>
|
|
</ion-item>
|
|
|
|
<ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
|
|
Registrarse
|
|
</ion-button>
|
|
</form>
|
|
</div>
|
|
```
|
|
|
|
**pages/auth/auth.page.html**:
|
|
|
|
```html
|
|
<ion-header>
|
|
<ion-toolbar color="primary">
|
|
<ion-title>Gym Reservation</ion-title>
|
|
</ion-toolbar>
|
|
</ion-header>
|
|
|
|
<ion-content class="ion-padding">
|
|
<div class="logo-container">
|
|
<img src="assets/shapes.svg" alt="Logo" class="logo">
|
|
<h1>Gym Reservation</h1>
|
|
<p>Tu app para reservar clases en tu gimnasio favorito</p>
|
|
</div>
|
|
|
|
<ion-segment [(ngModel)]="segment" (ionChange)="segmentChanged($event)">
|
|
<ion-segment-button value="login">
|
|
<ion-label>Iniciar Sesión</ion-label>
|
|
</ion-segment-button>
|
|
<ion-segment-button value="register">
|
|
<ion-label>Registrarse</ion-label>
|
|
</ion-segment-button>
|
|
</ion-segment>
|
|
|
|
<div [ngSwitch]="segment">
|
|
<app-login *ngSwitchCase="'login'" (loginSuccess)="onLoginSuccess()"></app-login>
|
|
<app-register *ngSwitchCase="'register'" (registerSuccess)="onRegisterSuccess()"></app-register>
|
|
</div>
|
|
|
|
<div class="demo-access">
|
|
<p>¿Quieres probar la app? Usa estos datos:</p>
|
|
<p><strong>Email:</strong> usuario@ejemplo.com</p>
|
|
<p><strong>Contraseña:</strong> password123</p>
|
|
</div>
|
|
</ion-content>
|
|
```
|
|
|
|
## 11. Servicio para Subir Imágenes
|
|
|
|
Implementemos un servicio para manejar la carga de imágenes al backend:
|
|
|
|
**services/upload.service.ts**:
|
|
|
|
```typescript
|
|
import { Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { Observable } from 'rxjs';
|
|
import { environment } from '../../environments/environment';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class UploadService {
|
|
private apiUrl = `${environment.apiUrl}/api/upload`;
|
|
|
|
constructor(private http: HttpClient) { }
|
|
|
|
/**
|
|
* Sube una imagen al servidor
|
|
* @param file Archivo a subir
|
|
* @param type Tipo de imagen (avatar, class, etc)
|
|
* @returns URL de la imagen subida
|
|
*/
|
|
uploadImage(file: File, type: 'avatar' | 'class' = 'avatar'): Observable<{ imageUrl: string }> {
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
formData.append('type', type);
|
|
|
|
return this.http.post<{ imageUrl: string }>(`${this.apiUrl}`, formData);
|
|
}
|
|
|
|
/**
|
|
* Convierte una imagen dataURL (base64) a un archivo File
|
|
* @param dataUrl URL de datos base64
|
|
* @param filename Nombre del archivo
|
|
*/
|
|
dataURLtoFile(dataUrl: string, filename: string): File {
|
|
const arr = dataUrl.split(',');
|
|
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
|
|
const bstr = atob(arr[1]);
|
|
let n = bstr.length;
|
|
const u8arr = new Uint8Array(n);
|
|
|
|
while (n--) {
|
|
u8arr[n] = bstr.charCodeAt(n);
|
|
}
|
|
|
|
return new File([u8arr], filename, { type: mime });
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resumen
|
|
|
|
En este capítulo, hemos establecido las bases sólidas para nuestra aplicación de reservas para gimnasio:
|
|
|
|
1. Creamos la estructura del proyecto
|
|
2. Implementamos los modelos de datos
|
|
3. Configuramos los servicios básicos
|
|
4. Establecimos la autenticación con un backend real
|
|
5. Implementamos las rutas principales
|
|
6. Creamos las páginas y componentes esenciales
|
|
7. Integramos el sistema de subida de imágenes
|
|
|
|
En el siguiente capítulo, completaremos la implementación con:
|
|
- Página de listado de clases
|
|
- Detalle de clase y reserva
|
|
- Gestión de reservas
|
|
- Perfil de usuario
|
|
- Integración con capacidades nativas mediante Capacitor
|
|
|
|
## Disclaimer
|
|
|
|
> **Nota importante:** Si durante el desarrollo encuentras errores o discrepancias en la ejecución del código, verifica la implementación correcta de cada módulo. El código completo de cada componente está disponible en el repositorio del curso. Asegúrate de revisar que los nombres de las clases, servicios e importaciones coincidan exactamente con lo especificado en este tutorial.
|
|
>
|
|
> Si tienes problemas con algún módulo específico, puedes verificar la estructura del proyecto y consultar la documentación oficial de Ionic y Angular para obtener más información sobre cómo resolver los errores más comunes.
|
|
> Adicionalmente igual puedes descargar el repositorio completo desde este [link](https://git.lcespedes.dev/lxluxo23/taller-ionic)
|
|
> Digo eso ya que es en ocasiones el ionic-cli funciona de manera distinta dependiendo de la versión de este mismo, además de la versión de node. |