taller-ionic/cap4.md
2025-04-24 15:57:53 -04:00

28 KiB

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)

# 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] enter image description here

Ú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:

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

proyecto,corriendo 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:

# 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:

# 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:

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:

export interface Booking {
  id: string;
  userId: string;
  classId: string;
  className: string;
  date: Date;
  status: 'confirmed' | 'cancelled' | 'pending';
}

user.model.ts:

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:

# 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

npm install @capacitor/preferences

Ahora implementemos el servicio de almacenamiento:

services/storage.service.ts:

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:

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'  // URL de la API de desarrollo
};

src/environments/environment.prod.ts:

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:

npm install @angular/common/http

Configuración del Módulo HTTP

Agreguemos el módulo HTTP al app.module.ts:

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:

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:

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:

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:

# 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:

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:

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

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:

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

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

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

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