Skip to content

Commit 5e4ca69

Browse files
committed
feat(daily-activities): adding page to view and mark as complete the exercises
1 parent c622dc8 commit 5e4ca69

10 files changed

Lines changed: 331 additions & 89 deletions

File tree

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
"@angular/platform-browser-dynamic": "^19.2.0",
1919
"@angular/router": "^19.2.0",
2020
"@angular/service-worker": "^19.2.0",
21+
"@angular/youtube-player": "^19.2.19",
2122
"@auth0/angular-jwt": "^5.2.0",
2223
"@bluehalo/ngx-leaflet": "^19.0.0",
2324
"@github/relative-time-element": "^4.4.5",
25+
"@stomp/rx-stomp": "^2.2.0",
2426
"@tailwindcss/postcss": "^4.0.15",
2527
"@types/leaflet": "^1.9.17",
2628
"daisyui": "^5.0.9",
2729
"echarts": "^6.0.0",
30+
"jwt-decode": "^4.0.0",
2831
"keycloak-angular": "^19.0.2",
2932
"keycloak-js": "24.0.2",
3033
"leaflet": "^1.9.3",
@@ -34,11 +37,9 @@
3437
"papaparse": "^5.5.2",
3538
"postcss": "^8.5.3",
3639
"rxjs": "~7.8.0",
40+
"sockjs-client": "^1.6.1",
3741
"tailwindcss": "^4.0.15",
3842
"tslib": "^2.3.0",
39-
"@stomp/rx-stomp": "^2.2.0",
40-
"sockjs-client": "^1.6.1",
41-
"jwt-decode": "^4.0.0",
4243
"zone.js": "~0.15.0"
4344
},
4445
"devDependencies": {

src/app/app.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const routes: Routes = [
3838
loadComponent: () => import('./modules/main/main.component'),
3939
children: [
4040
{ path: '', loadComponent: () => import('./modules/main/home/home.component') },
41+
{ path: 'exercises/:id', loadComponent: () => import('./modules/main/exercises/exercises.component') },
4142
{ path: 'mood', loadChildren: () => import('./modules/main/mood/mood.routes') },
4243
{ path: 'community', loadChildren: () => import('./modules/main/community/community.routes') },
4344
{ path: 'resources', loadChildren: () => import('./modules/main/resources/resources.routes') },

src/app/core/models/activity.model.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export interface ActivityCategory {
22
id: string;
3-
name: string ;
4-
description: string ;
3+
name: string;
4+
description: string;
55
imageUrl: string;
66
createdAt: string;
77
updatedAt: string;
@@ -28,6 +28,15 @@ export interface DailyExerciseResponse {
2828

2929
export type DailyExerciseRequest = Omit<DailyExerciseResponse, 'id' | 'categoryName' | 'contentTypeDisplay' | 'difficultyDisplay' | 'createdAt' | 'updatedAt'>
3030

31+
export interface AssignmentResponse {
32+
id: string;
33+
userId: string;
34+
exercise: Omit<DailyExerciseResponse, 'createdAt' | 'updatedAt'>;
35+
completed: boolean;
36+
assignedAt: string;
37+
completedAt: string;
38+
}
39+
3140
export enum ExerciseContentType {
3241
TEXTO = 'TEXTO',
3342
JUEGO = 'JUEGO',

src/app/core/services/daily-exercise.service.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,36 @@ import { inject, Injectable } from '@angular/core';
22
import { environment } from '../../../environments/environment';
33
import { HttpClient } from '@angular/common/http';
44
import { ApiResponse } from '../../shared/interfaces/api-response';
5-
import { DailyExerciseRequest, DailyExerciseResponse } from '../models/activity.model';
5+
import { AssignmentResponse, DailyExerciseRequest, DailyExerciseResponse } from '../models/activity.model';
66

77
@Injectable({
88
providedIn: 'root'
99
})
1010
export class DailyExerciseService {
1111
private http = inject(HttpClient);
12-
private baseUrl: string = environment.apiUrl + '/api/mind/daily-activity/exercises';
12+
private baseUrl: string = environment.apiUrl + '/api/mind/daily-activity';
1313

1414
getExercises() {
15-
return this.http.get<ApiResponse<DailyExerciseResponse[]>>(`${this.baseUrl}/difficulty/PRINCIPIANTE`);
15+
return this.http.get<ApiResponse<DailyExerciseResponse[]>>(`${this.baseUrl}/exercises/difficulty/PRINCIPIANTE`);
1616
}
1717

1818
createExercise(exercise: DailyExerciseRequest) {
19-
return this.http.post<ApiResponse<DailyExerciseResponse>>(`${this.baseUrl}`, exercise);
19+
return this.http.post<ApiResponse<DailyExerciseResponse>>(`${this.baseUrl}/exercises`, exercise);
2020
}
2121

2222
updateExercise(id: string, exercise: DailyExerciseRequest) {
23-
return this.http.put<ApiResponse<DailyExerciseResponse>>(`${this.baseUrl}/${id}`, exercise);
23+
return this.http.put<ApiResponse<DailyExerciseResponse>>(`${this.baseUrl}/exercises/${id}`, exercise);
2424
}
2525

2626
deleteExercise(id: string) {
27-
return this.http.delete<ApiResponse<string>>(`${this.baseUrl}/${id}`);
27+
return this.http.delete<ApiResponse<string>>(`${this.baseUrl}/exercises/${id}`);
28+
}
29+
30+
getMyDailyExercises() {
31+
return this.http.get<ApiResponse<AssignmentResponse[]>>(`${this.baseUrl}/assignments/my-exercises`);
32+
}
33+
34+
markAsCompleted(id: string, completed: boolean) {
35+
return this.http.patch<ApiResponse<AssignmentResponse>>(`${this.baseUrl}/assignments/${id}/complete`, { completed });
2836
}
2937
}

src/app/modules/admin/daily-exercise/daily-exercise-modal/exercise-modal.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export class ExerciseModalComponent implements OnInit {
212212
contentType: this.exercise()?.contentType,
213213
contentUrl: this.exercise()?.contentUrl,
214214
thumbnailUrl: this.exercise()?.thumbnailUrl,
215+
duration: this.exercise()?.durationMinutes,
215216
category: this.exercise()?.categoryId,
216217
difficulty: this.exercise()?.difficulty
217218
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<!--Contenedor principal-->
2+
<div class="p-4 sm:p-6 md:p-8">
3+
<div class="max-w-4xl mx-auto">
4+
5+
<!--1. Estado de Carga-->
6+
@if (activities.isLoading()) {
7+
<div class="space-y-6">
8+
<!--Skeleton del encabezado-->
9+
<div class="skeleton h-8 w-16 rounded-lg"></div>
10+
<div class="skeleton h-10 w-3/4 rounded-lg"></div>
11+
<div class="flex gap-2">
12+
<div class="skeleton h-6 w-24 rounded-full"></div>
13+
<div class="skeleton h-6 w-24 rounded-full"></div>
14+
<div class="skeleton h-6 w-24 rounded-full"></div>
15+
</div>
16+
<div class="divider"></div>
17+
<!--Skeleton del contenido-->
18+
<div class="skeleton h-64 w-full rounded-box"></div>
19+
<div class="space-y-2">
20+
<div class="skeleton h-4 w-full"></div>
21+
<div class="skeleton h-4 w-full"></div>
22+
<div class="skeleton h-4 w-3/4"></div>
23+
</div>
24+
<!--Skeleton del botón-->
25+
<div class="skeleton h-12 w-full mt-4 rounded-lg"></div>
26+
</div>
27+
} @else if (activities.error()) {
28+
<div class="alert alert-error">
29+
<i class="material-symbols-outlined">error</i>
30+
<span>Error al cargar el ejercicio. Por favor, intenta de nuevo.</span>
31+
<button class="btn btn-sm btn-ghost" (click)="reload()">Reintentar</button>
32+
</div>
33+
} @else if (!selectedExercise()) {
34+
<div role="alert" class="alert alert-warning">
35+
<i class="material-symbols-outlined">search_off</i>
36+
<div>
37+
<h3 class="font-bold">Ejercicio no encontrado</h3>
38+
<div class="text-xs">El ejercicio que buscas no existe o no está disponible.</div>
39+
</div>
40+
<a class="btn btn-sm" routerLink="/app">Volver al inicio</a>
41+
</div>
42+
}
43+
@else {
44+
@if (selectedExercise(); as exercise) {
45+
<div class="card bg-base-100 shadow-xl border border-base-300/50">
46+
<div class="card-body">
47+
<!--Encabezado con botón de regreso-->
48+
<div class="mb-4">
49+
<button class="btn btn-ghost btn-sm" (click)="goBack()">
50+
<i class="material-symbols-outlined">arrow_back</i>
51+
Volver
52+
</button>
53+
</div>
54+
55+
<!--Título y Metadatos-->
56+
<h1 class="card-title text-3xl md:text-4xl font-extrabold tracking-tight mb-2">{{ exercise.exercise.title }}</h1>
57+
<div class="flex flex-wrap gap-2 mb-4 text-sm">
58+
<div class="badge badge-primary badge-outline gap-2">
59+
<i class="material-symbols-outlined !text-base">bookmark</i>
60+
{{ exercise.exercise.categoryName }}
61+
</div>
62+
<div class="badge badge-secondary badge-outline gap-2">
63+
<i class="material-symbols-outlined !text-base">signal_cellular_alt</i>
64+
{{ exercise.exercise.difficultyDisplay }}
65+
</div>
66+
<div class="badge badge-accent badge-outline gap-2">
67+
<i class="material-symbols-outlined !text-base">timer</i>
68+
{{ exercise.exercise.durationMinutes }} min
69+
</div>
70+
</div>
71+
72+
<div class="divider"></div>
73+
74+
<!--Contenido dinámico del ejercicio-->
75+
<div class="">
76+
@switch (exercise.exercise.contentType) {
77+
@case ('VIDEO') {
78+
<!-- Reproductor de Video -->
79+
@if (exercise.exercise.contentType === 'VIDEO') {
80+
<div class="rounded-lg overflow-hidden aspect-video bg-neutral max-w-[72dvw]">
81+
<!-- Video de YouTube -->
82+
@if (youtubeVideoId(); as videoId) {
83+
<youtube-player
84+
[videoId]="videoId"
85+
[playerVars]="{ autoplay: 0, controls: 1, modestbranding: 1 }"
86+
/>
87+
} @else if (isStandardVideo()) {
88+
<!-- Video Estándar (MP4) -->
89+
<video class="w-full h-full" controls [src]="exercise.exercise.contentUrl"></video>
90+
}
91+
</div>
92+
}
93+
}
94+
@case ('TEXTO') {
95+
<!--Contenido de texto con imagen-->
96+
@if (exercise.exercise.thumbnailUrl) {
97+
<figure>
98+
<img [src]="exercise.exercise.thumbnailUrl" [alt]="exercise.exercise.title"
99+
class="rounded-box w-full object-cover mb-4">
100+
</figure>
101+
}
102+
<p [innerHTML]="exercise.exercise.description"></p>
103+
}
104+
@case ('AUDIO') {
105+
<div class="alert alert-info">
106+
<i class="material-symbols-outlined">headphones</i>
107+
<span>El contenido de audio estará disponible próximamente.</span>
108+
</div>
109+
}
110+
@case ('JUEGO') {
111+
<div class="alert alert-info">
112+
<i class="material-symbols-outlined">stadia_controller</i>
113+
<span>El juego interactivo estará disponible próximamente.</span>
114+
</div>
115+
}
116+
}
117+
</div>
118+
119+
<!--Descripción adicional (si aplica)-->
120+
@if (exercise.exercise.contentType !== 'TEXTO') {
121+
<div class="divider"></div>
122+
<div class="prose max-w-none">
123+
<h3 class="font-bold">Descripción</h3>
124+
<p>{{ exercise.exercise.description }}</p>
125+
</div>
126+
}
127+
128+
<!--Acción-->
129+
<div class="card-actions justify-center mt-8">
130+
<button class="btn btn-primary btn-wide" (click)="markAsComplete()">
131+
<i class="material-symbols-outlined">check_circle</i>
132+
Marcar como Completado
133+
</button>
134+
</div>
135+
</div>
136+
</div>
137+
}
138+
}
139+
</div>
140+
</div>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Component, computed, inject, signal } from '@angular/core';
2+
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
3+
import { DailyExerciseService } from '../../../core/services/daily-exercise.service';
4+
import { rxResource } from '@angular/core/rxjs-interop';
5+
import { map } from 'rxjs';
6+
import { ToastService } from '../../../core/services/toast.service';
7+
import { Location, CommonModule } from '@angular/common';
8+
import { YouTubePlayerModule } from '@angular/youtube-player';
9+
10+
@Component({
11+
selector: 'app-exercises',
12+
templateUrl: './exercises.component.html',
13+
imports: [CommonModule, YouTubePlayerModule, RouterLink] // <-- Módulo actualizado
14+
})
15+
export default class ExercisesComponent {
16+
private route = inject(ActivatedRoute);
17+
private router = inject(Router);
18+
private location = inject(Location);
19+
20+
private exercisesService = inject(DailyExerciseService);
21+
private toastService = inject(ToastService);
22+
23+
activities = rxResource({
24+
loader: () => this.exercisesService.getMyDailyExercises().pipe(
25+
map((response) => response.result)
26+
)
27+
});
28+
29+
exerciseId = signal('');
30+
31+
selectedExercise = computed(() => {
32+
const exercises = this.activities.value();
33+
if (!exercises) return undefined;
34+
35+
return exercises.find(exercise => exercise.id === this.exerciseId());
36+
});
37+
38+
youtubeVideoId = computed(() => {
39+
const exercise = this.selectedExercise();
40+
if (exercise && exercise.exercise.contentType === 'VIDEO' && exercise.exercise.contentUrl?.includes('youtube.com')) {
41+
// Extrae el ID de varias URL de YouTube (youtu.be, /embed, ?v=)
42+
const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^&?]+)/;
43+
const match = exercise.exercise.contentUrl.match(regex);
44+
return match ? match[1] : null;
45+
}
46+
return null;
47+
});
48+
49+
// Nueva señal para videos estándar (MP4, etc.)
50+
isStandardVideo = computed(() => {
51+
const exercise = this.selectedExercise();
52+
return exercise && exercise.exercise.contentType === 'VIDEO' && !!exercise.exercise.contentUrl && !this.youtubeVideoId();
53+
});
54+
55+
constructor() {
56+
this.route.paramMap.subscribe(params => {
57+
this.exerciseId.set(params.get('id') ?? '');
58+
});
59+
}
60+
61+
markAsComplete() {
62+
this.exercisesService.markAsCompleted(this.exerciseId(), true).subscribe({
63+
next: () => {
64+
this.toastService.addToast({
65+
message: '¡Ejercicio completado con éxito!',
66+
type: 'success',
67+
duration: 4000
68+
});
69+
this.router.navigate(['/app/home'], { fragment: 'daily-activities' });
70+
},
71+
error: () => {
72+
this.toastService.addToast({
73+
message: 'Ocurrió un error al marcar el ejercicio como completado.',
74+
type: 'error',
75+
duration: 4000
76+
});
77+
}
78+
});
79+
}
80+
81+
goBack() {
82+
this.location.back();
83+
}
84+
85+
reload() {
86+
this.activities.reload();
87+
}
88+
}

0 commit comments

Comments
 (0)