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

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)
![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.