cap 4
This commit is contained in:
parent
cf6be05994
commit
d20150506f
@ -136,7 +136,8 @@
|
|||||||
"cli": {
|
"cli": {
|
||||||
"schematicCollections": [
|
"schematicCollections": [
|
||||||
"@ionic/angular-toolkit"
|
"@ionic/angular-toolkit"
|
||||||
]
|
],
|
||||||
|
"analytics": false
|
||||||
},
|
},
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@ionic/angular-toolkit:component": {
|
"@ionic/angular-toolkit:component": {
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"@capacitor/core": "7.2.0",
|
"@capacitor/core": "7.2.0",
|
||||||
"@capacitor/haptics": "7.0.1",
|
"@capacitor/haptics": "7.0.1",
|
||||||
"@capacitor/keyboard": "7.0.1",
|
"@capacitor/keyboard": "7.0.1",
|
||||||
|
"@capacitor/preferences": "^7.0.1",
|
||||||
"@capacitor/status-bar": "7.0.1",
|
"@capacitor/status-bar": "7.0.1",
|
||||||
"@ionic/angular": "^8.0.0",
|
"@ionic/angular": "^8.0.0",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
@ -2768,6 +2769,15 @@
|
|||||||
"@capacitor/core": ">=7.0.0"
|
"@capacitor/core": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/preferences": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-XF9jOHzvoIBZLwZr/EX6aVaUO1d8Mx7TwBLQS33pYHOliCW5knT5KUkFOXNNYxh9qqODYesee9xuQIKNJpQBag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor/status-bar": {
|
"node_modules/@capacitor/status-bar": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"@capacitor/core": "7.2.0",
|
"@capacitor/core": "7.2.0",
|
||||||
"@capacitor/haptics": "7.0.1",
|
"@capacitor/haptics": "7.0.1",
|
||||||
"@capacitor/keyboard": "7.0.1",
|
"@capacitor/keyboard": "7.0.1",
|
||||||
|
"@capacitor/preferences": "^7.0.1",
|
||||||
"@capacitor/status-bar": "7.0.1",
|
"@capacitor/status-bar": "7.0.1",
|
||||||
"@ionic/angular": "^8.0.0",
|
"@ionic/angular": "^8.0.0",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
|
|||||||
@ -5,6 +5,22 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
|
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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({
|
@NgModule({
|
||||||
|
|||||||
8
src/app/models/booking.model.ts
Normal file
8
src/app/models/booking.model.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface Booking {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
classId: string;
|
||||||
|
className: string;
|
||||||
|
date: Date;
|
||||||
|
status: 'confirmed' | 'cancelled' | 'pending';
|
||||||
|
}
|
||||||
12
src/app/models/gym-class.model.ts
Normal file
12
src/app/models/gym-class.model.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface GymClass {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
instructor: string;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
maxCapacity: number;
|
||||||
|
currentBookings: number;
|
||||||
|
category?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}
|
||||||
10
src/app/models/user.model.ts
Normal file
10
src/app/models/user.model.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
profilePic?: string;
|
||||||
|
preferences?: {
|
||||||
|
notifications: boolean;
|
||||||
|
favoriteClasses?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/app/pages/bookings/bookings-routing.module.ts
Normal file
17
src/app/pages/bookings/bookings-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { BookingsPage } from './bookings.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: BookingsPage
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class BookingsPageRoutingModule {}
|
||||||
20
src/app/pages/bookings/bookings.module.ts
Normal file
20
src/app/pages/bookings/bookings.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { BookingsPageRoutingModule } from './bookings-routing.module';
|
||||||
|
|
||||||
|
import { BookingsPage } from './bookings.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
IonicModule,
|
||||||
|
BookingsPageRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [BookingsPage],
|
||||||
|
})
|
||||||
|
export class BookingsPageModule {}
|
||||||
@ -1,17 +1,13 @@
|
|||||||
<ion-header [translucent]="true">
|
<ion-header [translucent]="true">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>
|
<ion-title>bookings</ion-title>
|
||||||
Tab 2
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true">
|
||||||
<ion-header collapse="condense">
|
<ion-header collapse="condense">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title size="large">Tab 2</ion-title>
|
<ion-title size="large">bookings</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<app-explore-container name="Tab 2 page"></app-explore-container>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
17
src/app/pages/bookings/bookings.page.spec.ts
Normal file
17
src/app/pages/bookings/bookings.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { BookingsPage } from './bookings.page';
|
||||||
|
|
||||||
|
describe('BookingsPage', () => {
|
||||||
|
let component: BookingsPage;
|
||||||
|
let fixture: ComponentFixture<BookingsPage>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BookingsPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
16
src/app/pages/bookings/bookings.page.ts
Normal file
16
src/app/pages/bookings/bookings.page.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bookings',
|
||||||
|
templateUrl: './bookings.page.html',
|
||||||
|
styleUrls: ['./bookings.page.scss'],
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class BookingsPage implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
src/app/pages/class-detail/class-detail-routing.module.ts
Normal file
17
src/app/pages/class-detail/class-detail-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { ClassDetailPage } from './class-detail.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ClassDetailPage
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class ClassDetailPageRoutingModule {}
|
||||||
17
src/app/pages/class-detail/class-detail.module.ts
Normal file
17
src/app/pages/class-detail/class-detail.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { ClassDetailPageRoutingModule } from './class-detail-routing.module';
|
||||||
|
import { ClassDetailPage } from './class-detail.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
IonicModule,
|
||||||
|
ClassDetailPageRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [ClassDetailPage],
|
||||||
|
})
|
||||||
|
export class ClassDetailPageModule {}
|
||||||
79
src/app/pages/class-detail/class-detail.page.html
Normal file
79
src/app/pages/class-detail/class-detail.page.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar color="primary">
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button defaultHref="/tabs/classes"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>Detalle de Clase</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<div *ngIf="cargando" class="ion-text-center">
|
||||||
|
<ion-spinner></ion-spinner>
|
||||||
|
<p>Cargando información...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="gymClass && !cargando">
|
||||||
|
<ion-card>
|
||||||
|
<ion-img [src]="gymClass.imageUrl || 'assets/classes/default.jpg'" class="class-image"></ion-img>
|
||||||
|
|
||||||
|
<ion-card-header>
|
||||||
|
<ion-badge>{{ gymClass.category }}</ion-badge>
|
||||||
|
<ion-card-title class="ion-margin-top">{{ gymClass.name }}</ion-card-title>
|
||||||
|
<ion-card-subtitle>Instructor: {{ gymClass.instructor }}</ion-card-subtitle>
|
||||||
|
</ion-card-header>
|
||||||
|
|
||||||
|
<ion-card-content>
|
||||||
|
<p>{{ gymClass.description }}</p>
|
||||||
|
|
||||||
|
<ion-list lines="none">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="time-outline" slot="start" color="primary"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h3>Horario</h3>
|
||||||
|
<p>{{ gymClass.startTime | date:'EEEE, d MMM, h:mm a' }} - {{ gymClass.endTime | date:'h:mm a' }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="people-outline" slot="start" color="primary"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h3>Capacidad</h3>
|
||||||
|
<p>{{ gymClass.currentBookings }}/{{ gymClass.maxCapacity }} plazas ocupadas</p>
|
||||||
|
<ion-progress-bar [value]="gymClass.currentBookings / gymClass.maxCapacity"
|
||||||
|
[color]="getCapacityColor()"></ion-progress-bar>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<div class="ion-padding">
|
||||||
|
<ion-button expand="block" (click)="reservarClase()" [disabled]="isClassFull() || reservando">
|
||||||
|
<ion-spinner name="dots" *ngIf="reservando"></ion-spinner>
|
||||||
|
<span *ngIf="!reservando">Reservar Plaza</span>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-text color="medium" *ngIf="isClassFull()" class="ion-text-center">
|
||||||
|
<p>Lo sentimos, esta clase está completa.</p>
|
||||||
|
</ion-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-fab vertical="bottom" horizontal="end" slot="fixed" *ngIf="gymClass">
|
||||||
|
<ion-fab-button color="light">
|
||||||
|
<ion-icon name="share-social"></ion-icon>
|
||||||
|
</ion-fab-button>
|
||||||
|
<ion-fab-list side="top">
|
||||||
|
<ion-fab-button color="primary" (click)="compartir('whatsapp')">
|
||||||
|
<ion-icon name="logo-whatsapp"></ion-icon>
|
||||||
|
</ion-fab-button>
|
||||||
|
<ion-fab-button color="secondary" (click)="compartir('twitter')">
|
||||||
|
<ion-icon name="logo-twitter"></ion-icon>
|
||||||
|
</ion-fab-button>
|
||||||
|
<ion-fab-button color="tertiary" (click)="compartir('email')">
|
||||||
|
<ion-icon name="mail"></ion-icon>
|
||||||
|
</ion-fab-button>
|
||||||
|
</ion-fab-list>
|
||||||
|
</ion-fab>
|
||||||
|
</ion-content>
|
||||||
9
src/app/pages/class-detail/class-detail.page.scss
Normal file
9
src/app/pages/class-detail/class-detail.page.scss
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.class-image {
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-progress-bar {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
17
src/app/pages/class-detail/class-detail.page.spec.ts
Normal file
17
src/app/pages/class-detail/class-detail.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ClassDetailPage } from './class-detail.page';
|
||||||
|
|
||||||
|
describe('ClassDetailPage', () => {
|
||||||
|
let component: ClassDetailPage;
|
||||||
|
let fixture: ComponentFixture<ClassDetailPage>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ClassDetailPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
150
src/app/pages/class-detail/class-detail.page.ts
Normal file
150
src/app/pages/class-detail/class-detail.page.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { NavController, ToastController, AlertController } from '@ionic/angular';
|
||||||
|
import { GymClass } from '../../models/gym-class.model';
|
||||||
|
import { ClassesService } from '../../services/classes.service';
|
||||||
|
import { BookingsService } from '../../services/bookings.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-class-detail',
|
||||||
|
templateUrl: './class-detail.page.html',
|
||||||
|
styleUrls: ['./class-detail.page.scss'],
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class ClassDetailPage implements OnInit {
|
||||||
|
gymClass?: GymClass;
|
||||||
|
cargando = true;
|
||||||
|
reservando = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private navCtrl: NavController,
|
||||||
|
private classesService: ClassesService,
|
||||||
|
private bookingsService: BookingsService,
|
||||||
|
private toastCtrl: ToastController,
|
||||||
|
private alertCtrl: AlertController
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.cargarDatosClase();
|
||||||
|
}
|
||||||
|
|
||||||
|
ionViewWillEnter() {
|
||||||
|
this.cargarDatosClase();
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarDatosClase() {
|
||||||
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
|
if (!id) {
|
||||||
|
this.navCtrl.navigateBack('/tabs/classes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cargando = true;
|
||||||
|
this.classesService.getClassById(id).subscribe({
|
||||||
|
next: (gymClass) => {
|
||||||
|
if (gymClass) {
|
||||||
|
this.gymClass = gymClass;
|
||||||
|
} else {
|
||||||
|
this.navCtrl.navigateBack('/tabs/classes');
|
||||||
|
this.mostrarToast('Clase no encontrada', 'danger');
|
||||||
|
}
|
||||||
|
this.cargando = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error al cargar clase', error);
|
||||||
|
this.cargando = false;
|
||||||
|
this.navCtrl.navigateBack('/tabs/classes');
|
||||||
|
this.mostrarToast('Error al cargar la información', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isClassFull(): boolean {
|
||||||
|
if (!this.gymClass) return true;
|
||||||
|
return this.gymClass.currentBookings >= this.gymClass.maxCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCapacityColor(): string {
|
||||||
|
if (!this.gymClass) return 'primary';
|
||||||
|
|
||||||
|
const ratio = this.gymClass.currentBookings / this.gymClass.maxCapacity;
|
||||||
|
|
||||||
|
if (ratio >= 0.9) return 'danger';
|
||||||
|
if (ratio >= 0.7) return 'warning';
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
async reservarClase() {
|
||||||
|
if (!this.gymClass || this.isClassFull() || this.reservando) return;
|
||||||
|
|
||||||
|
this.reservando = true;
|
||||||
|
|
||||||
|
this.bookingsService.addBooking(this.gymClass.id, this.gymClass.name).subscribe({
|
||||||
|
next: async (booking) => {
|
||||||
|
this.mostrarToast(`¡Reserva confirmada para ${this.gymClass?.name}!`, 'success');
|
||||||
|
|
||||||
|
// Actualizar contador de la clase en local para UX
|
||||||
|
if (this.gymClass) {
|
||||||
|
this.gymClass.currentBookings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reservando = false;
|
||||||
|
|
||||||
|
// Mostrar alerta de confirmación
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: '¡Reserva Exitosa!',
|
||||||
|
message: `Has reservado una plaza para ${this.gymClass?.name}. ¿Deseas ver tus reservas?`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'No, seguir explorando',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Ver Mis Reservas',
|
||||||
|
handler: () => {
|
||||||
|
this.navCtrl.navigateForward('/tabs/bookings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await alert.present();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error al reservar', error);
|
||||||
|
this.mostrarToast('Error al realizar la reserva', 'danger');
|
||||||
|
this.reservando = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async mostrarToast(mensaje: string, color: string = 'primary') {
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: mensaje,
|
||||||
|
duration: 2000,
|
||||||
|
position: 'bottom',
|
||||||
|
color: color
|
||||||
|
});
|
||||||
|
toast.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
compartir(medio: string) {
|
||||||
|
if (!this.gymClass) return;
|
||||||
|
|
||||||
|
const mensaje = `¡He encontrado una clase de ${this.gymClass.name} con ${this.gymClass.instructor}!`;
|
||||||
|
|
||||||
|
switch (medio) {
|
||||||
|
case 'whatsapp':
|
||||||
|
// En una app real, integraríamos con el plugin de Social Sharing
|
||||||
|
this.mostrarToast('Compartiendo por WhatsApp...', 'success');
|
||||||
|
break;
|
||||||
|
case 'twitter':
|
||||||
|
this.mostrarToast('Compartiendo por Twitter...', 'success');
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
this.mostrarToast('Compartiendo por Email...', 'success');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/pages/classes/classes-routing.module.ts
Normal file
20
src/app/pages/classes/classes-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { ClassesPage } from './classes.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ClassesPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
loadChildren: () => import('../class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class ClassesPageRoutingModule {}
|
||||||
17
src/app/pages/classes/classes.module.ts
Normal file
17
src/app/pages/classes/classes.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { ClassesPageRoutingModule } from './classes-routing.module';
|
||||||
|
import { ClassesPage } from './classes.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
IonicModule,
|
||||||
|
ClassesPageRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [ClassesPage],
|
||||||
|
})
|
||||||
|
export class ClassesPageModule {}
|
||||||
57
src/app/pages/classes/classes.page.html
Normal file
57
src/app/pages/classes/classes.page.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar color="primary">
|
||||||
|
<ion-title>Clases Disponibles</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
<ion-searchbar placeholder="Buscar clases" (ionInput)="buscarClases($event)" [debounce]="500"></ion-searchbar>
|
||||||
|
|
||||||
|
<ion-segment (ionChange)="filtrarPorCategoria($event)" value="todas">
|
||||||
|
<ion-segment-button value="todas">
|
||||||
|
<ion-label>Todas</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="Mente y Cuerpo">
|
||||||
|
<ion-label>Mente/Cuerpo</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="Cardiovascular">
|
||||||
|
<ion-label>Cardio</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="Fuerza">
|
||||||
|
<ion-label>Fuerza</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
|
|
||||||
|
<div *ngIf="cargando" class="ion-text-center ion-padding">
|
||||||
|
<ion-spinner></ion-spinner>
|
||||||
|
<p>Cargando clases...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-list *ngIf="!cargando">
|
||||||
|
<ion-item *ngFor="let gymClass of clasesFiltradas" [routerLink]="['/tabs/classes', gymClass.id]" detail>
|
||||||
|
<ion-thumbnail slot="start">
|
||||||
|
<ion-img [src]="gymClass.imageUrl || 'assets/classes/default.jpg'"></ion-img>
|
||||||
|
</ion-thumbnail>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ gymClass.name }}</h2>
|
||||||
|
<p>{{ gymClass.startTime | date:'EEE, d MMM, h:mm a' }}</p>
|
||||||
|
<p>{{ gymClass.instructor }}</p>
|
||||||
|
<ion-note>
|
||||||
|
<ion-icon name="people-outline"></ion-icon>
|
||||||
|
{{ gymClass.currentBookings }}/{{ gymClass.maxCapacity }}
|
||||||
|
</ion-note>
|
||||||
|
</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="gymClass.category">{{ gymClass.category }}</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-refresher slot="fixed" (ionRefresh)="refrescarClases($event)">
|
||||||
|
<ion-refresher-content></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<div *ngIf="!cargando && clasesFiltradas.length === 0" class="ion-text-center ion-padding">
|
||||||
|
<ion-icon name="sad-outline" style="font-size: 48px;"></ion-icon>
|
||||||
|
<h3>No se encontraron clases</h3>
|
||||||
|
<p>Intenta con otros filtros o términos de búsqueda.</p>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
17
src/app/pages/classes/classes.page.spec.ts
Normal file
17
src/app/pages/classes/classes.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ClassesPage } from './classes.page';
|
||||||
|
|
||||||
|
describe('ClassesPage', () => {
|
||||||
|
let component: ClassesPage;
|
||||||
|
let fixture: ComponentFixture<ClassesPage>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ClassesPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
93
src/app/pages/classes/classes.page.ts
Normal file
93
src/app/pages/classes/classes.page.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { GymClass } from '../../models/gym-class.model';
|
||||||
|
import { ClassesService } from '../../services/classes.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-classes',
|
||||||
|
templateUrl: './classes.page.html',
|
||||||
|
styleUrls: ['./classes.page.scss'],
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class ClassesPage implements OnInit {
|
||||||
|
clases: GymClass[] = [];
|
||||||
|
clasesFiltradas: GymClass[] = [];
|
||||||
|
cargando = true;
|
||||||
|
terminoBusqueda = '';
|
||||||
|
categoriaSeleccionada = 'todas';
|
||||||
|
|
||||||
|
constructor(private classesService: ClassesService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.cargarClases();
|
||||||
|
}
|
||||||
|
|
||||||
|
ionViewWillEnter() {
|
||||||
|
this.cargarClases();
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarClases() {
|
||||||
|
this.cargando = true;
|
||||||
|
this.classesService.getClasses().subscribe({
|
||||||
|
next: (classes) => {
|
||||||
|
this.clases = classes;
|
||||||
|
this.aplicarFiltros();
|
||||||
|
this.cargando = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error al cargar clases', error);
|
||||||
|
this.cargando = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refrescarClases(event: any) {
|
||||||
|
this.classesService.getClasses().subscribe({
|
||||||
|
next: (classes) => {
|
||||||
|
this.clases = classes;
|
||||||
|
this.aplicarFiltros();
|
||||||
|
event.target.complete();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error al refrescar clases', error);
|
||||||
|
event.target.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buscarClases(event: any) {
|
||||||
|
this.terminoBusqueda = event.detail.value.toLowerCase();
|
||||||
|
this.aplicarFiltros();
|
||||||
|
}
|
||||||
|
|
||||||
|
filtrarPorCategoria(event: any) {
|
||||||
|
this.categoriaSeleccionada = event.detail.value;
|
||||||
|
this.aplicarFiltros();
|
||||||
|
}
|
||||||
|
|
||||||
|
private aplicarFiltros() {
|
||||||
|
let resultado = [...this.clases];
|
||||||
|
|
||||||
|
// Filtrar por término de búsqueda
|
||||||
|
if (this.terminoBusqueda) {
|
||||||
|
resultado = resultado.filter(clase =>
|
||||||
|
clase.name.toLowerCase().includes(this.terminoBusqueda) ||
|
||||||
|
clase.instructor.toLowerCase().includes(this.terminoBusqueda) ||
|
||||||
|
clase.description.toLowerCase().includes(this.terminoBusqueda)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar por categoría
|
||||||
|
if (this.categoriaSeleccionada !== 'todas') {
|
||||||
|
resultado = resultado.filter(clase =>
|
||||||
|
clase.category === this.categoriaSeleccionada
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar por fecha/hora
|
||||||
|
resultado.sort((a, b) => {
|
||||||
|
return a.startTime.getTime() - b.startTime.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clasesFiltradas = resultado;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/pages/profile/profile-routing.module.ts
Normal file
17
src/app/pages/profile/profile-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { ProfilePage } from './profile.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ProfilePage
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class ProfilePageRoutingModule {}
|
||||||
20
src/app/pages/profile/profile.module.ts
Normal file
20
src/app/pages/profile/profile.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { ProfilePageRoutingModule } from './profile-routing.module';
|
||||||
|
|
||||||
|
import { ProfilePage } from './profile.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
IonicModule,
|
||||||
|
ProfilePageRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [ProfilePage]
|
||||||
|
})
|
||||||
|
export class ProfilePageModule {}
|
||||||
@ -1,17 +1,13 @@
|
|||||||
<ion-header [translucent]="true">
|
<ion-header [translucent]="true">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>
|
<ion-title>profile</ion-title>
|
||||||
Tab 3
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
<ion-content [fullscreen]="true">
|
||||||
<ion-header collapse="condense">
|
<ion-header collapse="condense">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title size="large">Tab 3</ion-title>
|
<ion-title size="large">profile</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<app-explore-container name="Tab 3 page"></app-explore-container>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
17
src/app/pages/profile/profile.page.spec.ts
Normal file
17
src/app/pages/profile/profile.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ProfilePage } from './profile.page';
|
||||||
|
|
||||||
|
describe('ProfilePage', () => {
|
||||||
|
let component: ProfilePage;
|
||||||
|
let fixture: ComponentFixture<ProfilePage>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProfilePage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
16
src/app/pages/profile/profile.page.ts
Normal file
16
src/app/pages/profile/profile.page.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile',
|
||||||
|
templateUrl: './profile.page.html',
|
||||||
|
styleUrls: ['./profile.page.scss'],
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class ProfilePage implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/app/services/auth.service.spec.ts
Normal file
16
src/app/services/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
66
src/app/services/auth.service.ts
Normal file
66
src/app/services/auth.service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||||
|
import { delay, tap } from 'rxjs/operators';
|
||||||
|
import { User } from '../models/user.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||||
|
public currentUser$ = this.currentUserSubject.asObservable();
|
||||||
|
|
||||||
|
// Usuario de demostración
|
||||||
|
private demoUser: User = {
|
||||||
|
id: 'user123',
|
||||||
|
name: 'Usuario Demo',
|
||||||
|
email: 'usuario@ejemplo.com',
|
||||||
|
preferences: {
|
||||||
|
notifications: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Simular usuario ya autenticado para el taller
|
||||||
|
this.currentUserSubject.next(this.demoUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): User | null {
|
||||||
|
return this.currentUserSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulación de login
|
||||||
|
login(email: string, password: string): Observable<User> {
|
||||||
|
// En una app real, aquí se realizaría la autenticación contra un backend
|
||||||
|
return of(this.demoUser).pipe(
|
||||||
|
delay(1000), // Simular latencia de red
|
||||||
|
tap(user => this.currentUserSubject.next(user))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulación de logout
|
||||||
|
logout(): Observable<boolean> {
|
||||||
|
return of(true).pipe(
|
||||||
|
delay(500), // Simular latencia de red
|
||||||
|
tap(() => this.currentUserSubject.next(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulación de actualización de perfil
|
||||||
|
updateUserProfile(userData: Partial<User>): Observable<User> {
|
||||||
|
const currentUser = this.getCurrentUser();
|
||||||
|
if (!currentUser) {
|
||||||
|
return of(this.demoUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser: User = {
|
||||||
|
...currentUser,
|
||||||
|
...userData
|
||||||
|
};
|
||||||
|
|
||||||
|
return of(updatedUser).pipe(
|
||||||
|
delay(800), // Simular latencia de red
|
||||||
|
tap(user => this.currentUserSubject.next(user))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/services/bookings.service.spec.ts
Normal file
16
src/app/services/bookings.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BookingsService } from './bookings.service';
|
||||||
|
|
||||||
|
describe('BookingsService', () => {
|
||||||
|
let service: BookingsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(BookingsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
124
src/app/services/bookings.service.ts
Normal file
124
src/app/services/bookings.service.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable, from, of } from 'rxjs';
|
||||||
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { Booking } from '../models/booking.model';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
import { ClassesService } from './classes.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BookingsService {
|
||||||
|
private STORAGE_KEY = 'bookings';
|
||||||
|
private bookingsSubject = new BehaviorSubject<Booking[]>([]);
|
||||||
|
public bookings$ = this.bookingsSubject.asObservable();
|
||||||
|
private initialized = false;
|
||||||
|
private userId = 'user123'; // En una app real, vendría de la autenticación
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private storageService: StorageService,
|
||||||
|
private classesService: ClassesService
|
||||||
|
) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
const storedBookings = await this.storageService.get(this.STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedBookings) {
|
||||||
|
// Convertir las fechas de string a objetos Date
|
||||||
|
const bookings = storedBookings.map((booking: any) => ({
|
||||||
|
...booking,
|
||||||
|
date: new Date(booking.date)
|
||||||
|
}));
|
||||||
|
this.bookingsSubject.next(bookings);
|
||||||
|
} else {
|
||||||
|
this.bookingsSubject.next([]);
|
||||||
|
await this.storageService.set(this.STORAGE_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBookings(): Observable<Booking[]> {
|
||||||
|
return from(this.ensureInitialized()).pipe(
|
||||||
|
switchMap(() => this.bookings$),
|
||||||
|
map(bookings => bookings.filter(booking => booking.userId === this.userId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addBooking(classId: string, className: string): Observable<Booking> {
|
||||||
|
return from(this.ensureInitialized()).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
// Actualizar el contador de reservas de la clase
|
||||||
|
return this.classesService.updateClassBookings(classId, 1).pipe(
|
||||||
|
switchMap(success => {
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('No se pudo actualizar la clase');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBooking: Booking = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
userId: this.userId,
|
||||||
|
classId,
|
||||||
|
className,
|
||||||
|
date: new Date(),
|
||||||
|
status: 'confirmed'
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentBookings = this.bookingsSubject.value;
|
||||||
|
const updatedBookings = [...currentBookings, newBooking];
|
||||||
|
|
||||||
|
return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe(
|
||||||
|
tap(() => this.bookingsSubject.next(updatedBookings)),
|
||||||
|
map(() => newBooking)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelBooking(bookingId: string): Observable<boolean> {
|
||||||
|
return from(this.ensureInitialized()).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
const currentBookings = this.bookingsSubject.value;
|
||||||
|
const index = currentBookings.findIndex(b => b.id === bookingId);
|
||||||
|
|
||||||
|
if (index === -1) return of(false);
|
||||||
|
|
||||||
|
const booking = currentBookings[index];
|
||||||
|
|
||||||
|
// No permitir cancelar reservas ya canceladas
|
||||||
|
if (booking.status === 'cancelled') return of(false);
|
||||||
|
|
||||||
|
// Crear copia actualizada
|
||||||
|
const updatedBooking = { ...booking, status: 'cancelled' as 'cancelled' };
|
||||||
|
const updatedBookings = [...currentBookings];
|
||||||
|
updatedBookings[index] = updatedBooking;
|
||||||
|
|
||||||
|
// Actualizar contador de clase
|
||||||
|
return this.classesService.updateClassBookings(booking.classId, -1).pipe(
|
||||||
|
switchMap(success => {
|
||||||
|
if (!success) {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe(
|
||||||
|
tap(() => this.bookingsSubject.next(updatedBookings)),
|
||||||
|
map(() => true)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureInitialized(): Promise<void> {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/services/classes.service.spec.ts
Normal file
16
src/app/services/classes.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ClassesService } from './classes.service';
|
||||||
|
|
||||||
|
describe('ClassesService', () => {
|
||||||
|
let service: ClassesService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ClassesService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
155
src/app/services/classes.service.ts
Normal file
155
src/app/services/classes.service.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable, from, of } from 'rxjs';
|
||||||
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { GymClass } from '../models/gym-class.model';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ClassesService {
|
||||||
|
private STORAGE_KEY = 'gym_classes';
|
||||||
|
private classesSubject = new BehaviorSubject<GymClass[]>([]);
|
||||||
|
public classes$ = this.classesSubject.asObservable();
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
// Datos iniciales para mock
|
||||||
|
private initialClasses: GymClass[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Yoga',
|
||||||
|
description: 'Clase de yoga para todos los niveles',
|
||||||
|
instructor: 'María López',
|
||||||
|
startTime: new Date('2025-04-24T08:00:00'),
|
||||||
|
endTime: new Date('2025-04-24T09:00:00'),
|
||||||
|
maxCapacity: 15,
|
||||||
|
currentBookings: 8,
|
||||||
|
category: 'Mente y Cuerpo',
|
||||||
|
imageUrl: 'https://cdn-icons-png.flaticon.com/512/3456/3456464.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Spinning',
|
||||||
|
description: 'Clase de alta intensidad de ciclismo estático',
|
||||||
|
instructor: 'Juan Pérez',
|
||||||
|
startTime: new Date('2025-04-24T10:00:00'),
|
||||||
|
endTime: new Date('2025-04-24T11:00:00'),
|
||||||
|
maxCapacity: 20,
|
||||||
|
currentBookings: 15,
|
||||||
|
category: 'Cardiovascular',
|
||||||
|
imageUrl: 'https://cdn-icons-png.flaticon.com/512/805/805504.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Pilates (Pesas de agarre)',
|
||||||
|
description: 'Fortalecimiento de core y flexibilidad',
|
||||||
|
instructor: 'Ana García',
|
||||||
|
startTime: new Date('2025-04-24T16:00:00'),
|
||||||
|
endTime: new Date('2025-04-24T17:00:00'),
|
||||||
|
maxCapacity: 12,
|
||||||
|
currentBookings: 5,
|
||||||
|
category: 'Mente y Cuerpo',
|
||||||
|
imageUrl: 'https://cdn-icons-png.flaticon.com/512/625/625454.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Zumba',
|
||||||
|
description: 'Baile y ejercicio cardiovascular',
|
||||||
|
instructor: 'Carlos Martínez',
|
||||||
|
startTime: new Date('2025-04-25T18:00:00'),
|
||||||
|
endTime: new Date('2025-04-25T19:00:00'),
|
||||||
|
maxCapacity: 25,
|
||||||
|
currentBookings: 18,
|
||||||
|
category: 'Baile',
|
||||||
|
imageUrl: 'https://cdn-icons-png.flaticon.com/512/5776/5776440.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'CrossFit',
|
||||||
|
description: 'Entrenamiento funcional de alta intensidad',
|
||||||
|
instructor: 'Roberto Sánchez',
|
||||||
|
startTime: new Date('2025-04-25T09:00:00'),
|
||||||
|
endTime: new Date('2025-04-25T10:00:00'),
|
||||||
|
maxCapacity: 15,
|
||||||
|
currentBookings: 12,
|
||||||
|
category: 'Fuerza',
|
||||||
|
imageUrl: 'https://cdn-icons-png.flaticon.com/512/372/372612.png'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private storageService: StorageService) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Intentar cargar datos desde almacenamiento
|
||||||
|
const storedClasses = await this.storageService.get(this.STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedClasses && storedClasses.length > 0) {
|
||||||
|
// Convertir las fechas de string a objetos Date
|
||||||
|
const classes = storedClasses.map((cls: any) => ({
|
||||||
|
...cls,
|
||||||
|
startTime: new Date(cls.startTime),
|
||||||
|
endTime: new Date(cls.endTime)
|
||||||
|
}));
|
||||||
|
this.classesSubject.next(classes);
|
||||||
|
} else {
|
||||||
|
// Si no hay datos almacenados, usar datos iniciales
|
||||||
|
await this.storageService.set(this.STORAGE_KEY, this.initialClasses);
|
||||||
|
this.classesSubject.next(this.initialClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClasses(): Observable<GymClass[]> {
|
||||||
|
return from(this.ensureInitialized()).pipe(
|
||||||
|
switchMap(() => this.classes$)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClassById(id: string): Observable<GymClass | undefined> {
|
||||||
|
return this.getClasses().pipe(
|
||||||
|
map(classes => classes.find(c => c.id === id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClassBookings(classId: string, change: number): Observable<boolean> {
|
||||||
|
return from(this.ensureInitialized()).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
const currentClasses = this.classesSubject.value;
|
||||||
|
const index = currentClasses.findIndex(c => c.id === classId);
|
||||||
|
|
||||||
|
if (index === -1) return of(false);
|
||||||
|
|
||||||
|
const updatedClass = { ...currentClasses[index] };
|
||||||
|
updatedClass.currentBookings += change;
|
||||||
|
|
||||||
|
// Verificar límites
|
||||||
|
if (updatedClass.currentBookings < 0) {
|
||||||
|
updatedClass.currentBookings = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedClass.currentBookings > updatedClass.maxCapacity) {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedClasses = [...currentClasses];
|
||||||
|
updatedClasses[index] = updatedClass;
|
||||||
|
|
||||||
|
return from(this.storageService.set(this.STORAGE_KEY, updatedClasses)).pipe(
|
||||||
|
tap(() => this.classesSubject.next(updatedClasses)),
|
||||||
|
map(() => true)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureInitialized(): Promise<void> {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/services/storage.service.spec.ts
Normal file
16
src/app/services/storage.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
|
describe('StorageService', () => {
|
||||||
|
let service: StorageService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(StorageService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
src/app/services/storage.service.ts
Normal file
35
src/app/services/storage.service.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { Tab1Page } from './tab1.page';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: Tab1Page,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class Tab1PageRoutingModule {}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { Tab1Page } from './tab1.page';
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab1PageRoutingModule } from './tab1-routing.module';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
IonicModule,
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
ExploreContainerComponentModule,
|
|
||||||
Tab1PageRoutingModule
|
|
||||||
],
|
|
||||||
declarations: [Tab1Page]
|
|
||||||
})
|
|
||||||
export class Tab1PageModule {}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<ion-header [translucent]="true">
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-title>
|
|
||||||
Tab 1
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content [fullscreen]="true">
|
|
||||||
<ion-header collapse="condense">
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-title size="large">Tab 1</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<app-explore-container name="Tab 1 page"></app-explore-container>
|
|
||||||
</ion-content>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab1Page } from './tab1.page';
|
|
||||||
|
|
||||||
describe('Tab1Page', () => {
|
|
||||||
let component: Tab1Page;
|
|
||||||
let fixture: ComponentFixture<Tab1Page>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [Tab1Page],
|
|
||||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(Tab1Page);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-tab1',
|
|
||||||
templateUrl: 'tab1.page.html',
|
|
||||||
styleUrls: ['tab1.page.scss'],
|
|
||||||
standalone: false,
|
|
||||||
})
|
|
||||||
export class Tab1Page {
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { Tab2Page } from './tab2.page';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: Tab2Page,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class Tab2PageRoutingModule {}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { Tab2Page } from './tab2.page';
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab2PageRoutingModule } from './tab2-routing.module';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
IonicModule,
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
ExploreContainerComponentModule,
|
|
||||||
Tab2PageRoutingModule
|
|
||||||
],
|
|
||||||
declarations: [Tab2Page]
|
|
||||||
})
|
|
||||||
export class Tab2PageModule {}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab2Page } from './tab2.page';
|
|
||||||
|
|
||||||
describe('Tab2Page', () => {
|
|
||||||
let component: Tab2Page;
|
|
||||||
let fixture: ComponentFixture<Tab2Page>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [Tab2Page],
|
|
||||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(Tab2Page);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-tab2',
|
|
||||||
templateUrl: 'tab2.page.html',
|
|
||||||
styleUrls: ['tab2.page.scss'],
|
|
||||||
standalone: false,
|
|
||||||
})
|
|
||||||
export class Tab2Page {
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { Tab3Page } from './tab3.page';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: Tab3Page,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class Tab3PageRoutingModule {}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { Tab3Page } from './tab3.page';
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab3PageRoutingModule } from './tab3-routing.module';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
IonicModule,
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
ExploreContainerComponentModule,
|
|
||||||
Tab3PageRoutingModule
|
|
||||||
],
|
|
||||||
declarations: [Tab3Page]
|
|
||||||
})
|
|
||||||
export class Tab3PageModule {}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { IonicModule } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
|
||||||
|
|
||||||
import { Tab3Page } from './tab3.page';
|
|
||||||
|
|
||||||
describe('Tab3Page', () => {
|
|
||||||
let component: Tab3Page;
|
|
||||||
let fixture: ComponentFixture<Tab3Page>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [Tab3Page],
|
|
||||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(Tab3Page);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-tab3',
|
|
||||||
templateUrl: 'tab3.page.html',
|
|
||||||
styleUrls: ['tab3.page.scss'],
|
|
||||||
standalone: false,
|
|
||||||
})
|
|
||||||
export class Tab3Page {
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -8,27 +8,27 @@ const routes: Routes = [
|
|||||||
component: TabsPage,
|
component: TabsPage,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'tab1',
|
path: 'classes',
|
||||||
loadChildren: () => import('../tab1/tab1.module').then(m => m.Tab1PageModule)
|
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tab2',
|
path: 'bookings',
|
||||||
loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
|
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tab3',
|
path: 'profile',
|
||||||
loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule)
|
loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: '/tabs/tab1',
|
redirectTo: '/tabs/classes',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: '/tabs/tab1',
|
redirectTo: '/tabs/classes',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -36,4 +36,4 @@ const routes: Routes = [
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
})
|
})
|
||||||
export class TabsPageRoutingModule {}
|
export class TabsPageRoutingModule {}
|
||||||
@ -1,20 +1,18 @@
|
|||||||
<ion-tabs>
|
<ion-tabs>
|
||||||
|
|
||||||
<ion-tab-bar slot="bottom">
|
<ion-tab-bar slot="bottom">
|
||||||
<ion-tab-button tab="tab1" href="/tabs/tab1">
|
<ion-tab-button tab="classes">
|
||||||
<ion-icon aria-hidden="true" name="triangle"></ion-icon>
|
<ion-icon name="calendar"></ion-icon>
|
||||||
<ion-label>Tab 1</ion-label>
|
<ion-label>Clases</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
|
|
||||||
<ion-tab-button tab="tab2" href="/tabs/tab2">
|
<ion-tab-button tab="bookings">
|
||||||
<ion-icon aria-hidden="true" name="ellipse"></ion-icon>
|
<ion-icon name="bookmark"></ion-icon>
|
||||||
<ion-label>Tab 2</ion-label>
|
<ion-label>Mis Reservas</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
|
|
||||||
<ion-tab-button tab="tab3" href="/tabs/tab3">
|
<ion-tab-button tab="profile">
|
||||||
<ion-icon aria-hidden="true" name="square"></ion-icon>
|
<ion-icon name="person"></ion-icon>
|
||||||
<ion-label>Tab 3</ion-label>
|
<ion-label>Perfil</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
|
|
||||||
</ion-tabs>
|
</ion-tabs>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user