Repository: IT-Academy-BCN/ita-challenges-frontend
Pull request #563: Feature/166 mentor assign tags to challenge
Author: login: mcallejo-10
Created: 2025-03-27T18:05:57Z | Merged: 2025-04-02T14:44:16Z

# Challenge Form Tags Implementation

## Changes
- Implement dynamic tag loading based on selected programming language
- Add multi-select functionality for tags
- Update challenge creation to include selected tags
- Create mock data files for tags by programming language (temporary solution)
- Integrate tag selection with form submission

## Technical Details
- Created mock data structure for tags per language while endpoints are developed
- Tags are dynamically loaded when a language is selected
- Tags can be toggled on/off with a single click
- Selected tags are visually highlighted
- Tags are properly cleared when switching languages
- Tags are included in the final challenge object on submission

## Design Notes
While the implementation differs from the original Figma design, the current multi-select tag system was agreed upon with the Scrum Master to enhance UX by allowing users to select multiple tags simultaneously. 



Diff:
diff --git a/src/app/core/layout/header/desktop-nav/desktop-nav.component.ts b/src/app/core/layout/header/desktop-nav/desktop-nav.component.ts
index 4b411ff62..9025f0b79 100644
--- a/src/app/core/layout/header/desktop-nav/desktop-nav.component.ts
+++ b/src/app/core/layout/header/desktop-nav/desktop-nav.component.ts
@@ -1,4 +1,6 @@
-import { Component, Inject } from '@angular/core'
+import { Component, Inject, OnDestroy, OnInit } from '@angular/core'
+import { Subscription } from 'rxjs';
+import { AuthService } from 'src/app/services/auth.service'
 import { NavService } from 'src/app/services/nav.service'
 
 @Component({
@@ -6,12 +8,25 @@ import { NavService } from 'src/app/services/nav.service'
   templateUrl: './desktop-nav.component.html',
   styleUrl: './desktop-nav.component.scss'
 })
-export class DesktopNavComponent {
+export class DesktopNavComponent implements OnInit, OnDestroy{
   isLoggedIn = false
+  private authSubscription!: Subscription;
 
-  constructor (@Inject(NavService) public navService: NavService) {
-    this.isLoggedIn = !(localStorage.getItem('authToken') == null)
-  }
+  constructor (
+    @Inject(NavService) public navService: NavService,
+    @Inject(AuthService) private _authService: AuthService) {}
+
+    ngOnInit(): void {
+      this.authSubscription = this._authService.isLoggedIn$.subscribe(isLoggedIn => {
+        this.isLoggedIn = isLoggedIn;
+      });
+    }
+
+    ngOnDestroy(): void {
+      if (this.authSubscription) {
+        this.authSubscription.unsubscribe();
+      }
+    }
 
   changeLanguage (event: Event): void {
     const selectElement = event.target as HTMLSelectElement
diff --git a/src/app/models/create-challenge.interface.ts b/src/app/models/create-challenge.interface.ts
index 1b2022083..4712fab81 100644
--- a/src/app/models/create-challenge.interface.ts
+++ b/src/app/models/create-challenge.interface.ts
@@ -11,4 +11,5 @@ export interface CreateChallenge {
   level: 'EASY' | 'MEDIUM' | 'HARD'
   language: string
   solution: string
+  tags: string[]
 }
diff --git a/src/app/modules/challenge/components/challenge-form/challenge-form.component.css b/src/app/modules/challenge/components/challenge-form/challenge-form.component.css
index 8bc84f956..ce2d38296 100644
--- a/src/app/modules/challenge/components/challenge-form/challenge-form.component.css
+++ b/src/app/modules/challenge/components/challenge-form/challenge-form.component.css
@@ -307,4 +307,45 @@
   .editor-container, .codemirror-container, .form-control {
     width: 68%;
   }
-}
\ No newline at end of file
+}
+
+
+/* TAGS */
+.tags-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-top: 8px;
+}
+
+.tag-button {
+  padding: 5px 12px;
+  border: 1px solid #e0e0e0;
+  border-radius: 16px;
+  background: white;
+  color: #666;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  line-height: 1.2;
+  font-weight: 500;
+}
+
+.tag-button:hover {
+  background: #f5f5f5;
+  border-color: #ccc;
+}
+
+.tag-button.active {
+  background: #C13584;
+  color: white;
+  border-color: #C13584;
+}
+
+@media (max-width: 768px) {
+  .tags-group {
+    width: 100%;
+  }
+}
+
+
diff --git a/src/app/modules/challenge/components/challenge-form/challenge-form.component.html b/src/app/modules/challenge/components/challenge-form/challenge-form.component.html
index 1748f6f17..4b5a02100 100644
--- a/src/app/modules/challenge/components/challenge-form/challenge-form.component.html
+++ b/src/app/modules/challenge/components/challenge-form/challenge-form.component.html
@@ -1,5 +1,5 @@
 
-  <div>
+<div>
     <!-- Header con título y botones -->
     <div class="header">
       <div class="header-content">
@@ -144,6 +144,23 @@ <h4><span class="section-number">3</span> Información adicional</h4>
               </div>
             </div>
           </div>
+          @if (challenge.language) {
+            <div class="form-group mb-4">
+              <label class="form-label">Tags</label>
+              <div class="tags-group">
+                @for (tag of currentTags; track tag.id_tag) {
+                  <button 
+                    type="button" 
+                    class="tag-button"
+                    [class.active]="isTagSelected(tag.id_tag)"
+                    (click)="onTagSelect(tag.id_tag)"
+                    [title]="tag.tag_description">
+                    {{ tag.tag_name }}
+                  </button>
+                }
+              </div>
+            </div>
+          }
         </div>
       </form>
     </div>
diff --git a/src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts b/src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts
index dbae943d4..22fa8ed6c 100644
--- a/src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts
+++ b/src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts
@@ -53,6 +53,31 @@ describe('ChallengeFormComponent', () => {
   let mockChallengeService: jest.Mocked<ChallengeService>
   let mockRouter: jest.Mocked<Router>
 
+  const mockJavascriptTags = {
+    results: [
+      {
+        id_tag: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
+        tag_name: 'Promises',
+        tag_description: 'Retos sobre programación asíncrona y manejo de promesas.'
+      },
+      {
+        id_tag: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
+        tag_name: 'DOM',
+        tag_description: 'Ejercicios de manipulación del Document Object Model.'
+      },
+      {
+        id_tag: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
+        tag_name: 'Arrays',
+        tag_description: 'Desafíos utilizando métodos de array como map, filter, reduce.'
+      },
+      {
+        id_tag: 'dddddddd-dddd-dddd-dddd-dddddddddddd',
+        tag_name: 'Closures',
+        tag_description: 'Retos sobre scope y closures en JavaScript.'
+      }
+    ]
+  }
+
   beforeEach(async () => {
     mockChallengeFormService = {
       getAllLangugesCreateForm: jest.fn().mockReturnValue(of({
@@ -235,4 +260,30 @@ describe('ChallengeFormComponent', () => {
     component.onCancel()
     expect(mockRouter.navigate).toHaveBeenCalledWith(['/ita-challenge/challenges'])
   })
+
+  describe('Tag Management', () => {
+    it('should load tags when language is selected', () => {
+      component.onLanguageChange('Javascript')
+      expect(component.currentTags).toEqual(mockJavascriptTags.results)
+    })
+
+    it('should clear selected tags when language changes', () => {
+      component.selectedTags = ['1', '2']
+      expect(component.selectedTags.length).toBe(2)
+      component.onLanguageChange('Python')
+      expect(component.selectedTags).toEqual([])
+    })
+
+    it('should toggle tag selection correctly', () => {
+      const testTagId = '1'
+      // Selecting a tag
+      component.onTagSelect(testTagId)
+      expect(component.selectedTags).toContain(testTagId)
+      expect(component.isTagSelected(testTagId)).toBeTruthy()
+      // Deselecting the same tag
+      component.onTagSelect(testTagId)
+      expect(component.selectedTags).not.toContain(testTagId)
+      expect(component.isTagSelected(testTagId)).toBeFalsy()
+    })
+  })
 })
diff --git a/src/app/modules/challenge/components/challenge-form/challenge-form.component.ts b/src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
index 66b49da59..81acb6fb5 100644
--- a/src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
+++ b/src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
@@ -16,6 +16,9 @@ import { java } from '@codemirror/lang-java'
 import { python } from '@codemirror/lang-python'
 import { basicSetup } from 'codemirror'
 
+// Añadir el import al inicio del archivo, temporalmente
+import { phpTags, javaTags, javascriptTags, sqlTags, pythonTags, typescriptTags } from 'src/mocks/challenge/tags.mock'
+
 @Component({
   standalone: true,
   selector: 'app-challenge-form',
@@ -33,10 +36,13 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     description: '',
     level: 'EASY',
     language: '' as string,
-    solution: ''
+    solution: '',
+    tags: []
   }
 
   languages: Language[] = []
+  selectedTags: string[] = []
+  currentTags: any[] = []
 
   editorConfig = {
     base_url: '/assets/tinymce',
@@ -128,15 +134,11 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     return extensions[language as keyof typeof extensions] || javascript
   }
 
-  // Carga los lenguajes del backend
-  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
   loadLanguages (): void {
     this.challengeFormService.getAllLangugesCreateForm().subscribe({
       next: ({ results }) => {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
         this.languages = results || []
-        console.log('Idiomas cargados:', this.languages) // Lista completa de idiomas
-        console.log(JSON.stringify(this.languages, null, 2))
       },
       error: (err) => {
         console.error('Error al obtener los idiomas:', err)
@@ -145,10 +147,9 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     })
   }
 
-  // Nuevo método para manejar el cambio de lenguaje
-  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-  onLanguageChange (language: string) {
+  onLanguageChange (language: string): void {
     this.challenge.language = language
+    this.loadTagsForLanguage(language)
     /* istanbul ignore next */
     // Actualiza CodeMirror con el nuevo lenguaje
     if (this.editor != null) {
@@ -168,6 +169,21 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     }
   }
 
+  private loadTagsForLanguage (language: string): void {
+    const tagMap = {
+      PHP: phpTags,
+      Java: javaTags,
+      Javascript: javascriptTags,
+      SQL: sqlTags,
+      Python: pythonTags,
+      Typescript: typescriptTags
+    }
+
+    const selectedLanguageTags = tagMap[language as keyof typeof tagMap]
+    this.currentTags = selectedLanguageTags?.results ?? []
+    this.selectedTags = []
+  }
+
   // Validación del formulario (código original)
   public isFormValid (): boolean {
     const isLanguageValid = this.languages.some(lang =>
@@ -194,6 +210,8 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
       return
     }
 
+    this.challenge.tags = [...this.selectedTags]
+
     this.challengeService.createChallenge(this.challenge).subscribe({
       next: (response) => {
         console.log('Reto creado:', response)
@@ -205,4 +223,17 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
       }
     })
   }
+
+  onTagSelect (idTag: string): void {
+    const index = this.selectedTags.indexOf(idTag)
+    if (index === -1) {
+      this.selectedTags.push(idTag)
+    } else {
+      this.selectedTags.splice(index, 1)
+    }
+  }
+
+  isTagSelected (idTag: string): boolean {
+    return this.selectedTags.includes(idTag)
+  }
 }
diff --git a/src/app/modules/challenge/components/challenge-info/challenge-info.component.spec.ts b/src/app/modules/challenge/components/challenge-info/challenge-info.component.spec.ts
index 383c1921f..bf83f0c94 100755
--- a/src/app/modules/challenge/components/challenge-info/challenge-info.component.spec.ts
+++ b/src/app/modules/challenge/components/challenge-info/challenge-info.component.spec.ts
@@ -51,17 +51,6 @@ describe('ChallengeInfoComponent', () => {
     expect(component).toBeTruthy()
   })
 
-  describe('ngOnInit', () => {
-    it('should call loadRelatedChallenges with the provided idChallenge', async () => { // Añadido async aquí
-      const loadRelatedChallengesSpy = jest.spyOn(component, 'loadRelatedChallenges')
-      component.idChallenge = '123'
-      await component.ngOnInit() // Ahora el await se permite dentro de la función marcada como async
-
-      expect(loadRelatedChallengesSpy).toHaveBeenCalledTimes(1)
-      expect(loadRelatedChallengesSpy).toHaveBeenCalledWith('123')
-    })
-  })
-
   it('should open send solution modal', () => {
     jest.spyOn(modalService, 'open').mockImplementation()
     component.openSendSolutionModal()
diff --git a/src/app/modules/mentor/mentor-login/mentor-login.component.ts b/src/app/modules/mentor/mentor-login/mentor-login.component.ts
index 22df80810..6b2a07628 100644
--- a/src/app/modules/mentor/mentor-login/mentor-login.component.ts
+++ b/src/app/modules/mentor/mentor-login/mentor-login.component.ts
@@ -80,8 +80,8 @@ export class MentorLoginComponent implements OnInit {
     this.http.post<GitHubAuthResponse>(url, { code }).subscribe({
       next: (response) => {
         if (response.isValid) {
-          localStorage.setItem('username', response.username)
           localStorage.setItem('authToken', response.token)
+          localStorage.setItem('username', response.username)
           
           this.authService.updateUserRoleFromToken()
 
diff --git a/src/app/services/auth.service.spec.ts b/src/app/services/auth.service.spec.ts
index 19d41e80e..685406ad6 100644
--- a/src/app/services/auth.service.spec.ts
+++ b/src/app/services/auth.service.spec.ts
@@ -1,13 +1,18 @@
 import { TestBed, fakeAsync, tick } from '@angular/core/testing';
 import { AuthService } from './auth.service';
+import { CookieService } from 'ngx-cookie-service';
 import { first } from 'rxjs/operators';
 
 describe('AuthService', () => {
   let service: AuthService;
+  let cookieService: CookieService;
 
   beforeEach(() => {
-    TestBed.configureTestingModule({});
+    TestBed.configureTestingModule({
+      providers: [CookieService]
+    });
     service = TestBed.inject(AuthService);
+    cookieService = TestBed.inject(CookieService);
   });
 
   afterEach(() => {
@@ -82,4 +87,15 @@ describe('AuthService', () => {
     tick();
     expect(updatedRole).toBe('');
   }));
+
+  it('should return true if user is logged in (auth token exists)', () => {
+    localStorage.setItem('authToken', 'test-token');
+    expect(service.isUserLoggedIn()).toBe(true);
+  });
+
+  it('should return false if user is not logged in (no auth token)', () => {
+    localStorage.removeItem('authToken');
+    expect(service.isUserLoggedIn()).toBe(false);
+  });
+
 });
\ No newline at end of file
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts
index 4a91c9ef7..05967cd66 100644
--- a/src/app/services/auth.service.ts
+++ b/src/app/services/auth.service.ts
@@ -1,4 +1,5 @@
 import { Injectable } from '@angular/core';
+import { CookieService } from 'ngx-cookie-service';
 import { Observable, of, BehaviorSubject } from 'rxjs';
 import { map } from 'rxjs/operators';
 
@@ -8,9 +9,10 @@ import { map } from 'rxjs/operators';
 export class AuthService {
   private userRole: string = '';
   private userRoleSubject = new BehaviorSubject<string>('');
+  private isLoggedInSubject = new BehaviorSubject<boolean>(this.checkAuthToken());
+  public isLoggedIn$: Observable<boolean> = this.isLoggedInSubject.asObservable();
 
-  constructor() {
-    // Initialize the role from localStorage on service creation
+  constructor(private _cookieService: CookieService) {
     this.updateUserRoleFromToken();
   }
 
@@ -21,15 +23,29 @@ export class AuthService {
   // Method to update the user role when authentication changes
   updateUserRoleFromToken(): void {
     const token = localStorage.getItem('authToken');
-    if (!token) {
-      this.userRole = '';
+    if (token) {
+      const decodedToken = this.decodeToken(token);
+      this.userRole = decodedToken?.role ?? '';
+      this.userRoleSubject.next(this.userRole);
+    } else {
       this.userRoleSubject.next('');
-      return;
     }
-    
-    const decodedToken = this.decodeToken(token);
-    this.userRole = decodedToken?.role ?? '';
-    this.userRoleSubject.next(this.userRole);
+    this.updateAuthStatus();
+  }
+
+  isUserLoggedIn(): boolean {
+    return localStorage.getItem('authToken') !== null;
+  }
+
+
+  private checkAuthToken(): boolean {
+    return localStorage.getItem('authToken') !== null;
+
+  }
+
+  private updateAuthStatus(): void {
+    const isLoggedIn = this.checkAuthToken();
+    this.isLoggedInSubject.next(isLoggedIn);
   }
 
   private decodeToken(token: string): any {
diff --git a/src/app/services/challenge.service.spec.ts b/src/app/services/challenge.service.spec.ts
index b2e1f0a5c..5e678e963 100755
--- a/src/app/services/challenge.service.spec.ts
+++ b/src/app/services/challenge.service.spec.ts
@@ -116,9 +116,10 @@ describe('ChallengeService', () => {
     const mockChallenge: CreateChallenge = {
       challengeTitle: 'Test Challenge',
       description: 'Test Description',
-      level: 'EASY', // ✅ Asegurar que sea 'EASY' | 'MEDIUM' | 'HARD'
+      level: 'EASY',
       language: 'Java',
-      solution: 'Test Solution'
+      solution: 'Test Solution',
+      tags: [] // Add the required tags property
     }
 
     const mockResponse = { id: 1, ...mockChallenge }
diff --git a/src/mocks/challenge/tags.mock.ts b/src/mocks/challenge/tags.mock.ts
new file mode 100644
index 000000000..3a1bd46b5
--- /dev/null
+++ b/src/mocks/challenge/tags.mock.ts
@@ -0,0 +1,178 @@
+interface TagResponse {
+  offset: number
+  limit: number
+  count: number
+  results: Array<{
+    id_tag: string
+    tag_name: string
+    tag_description: string
+  }>
+}
+
+export const phpTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: '00000000-0000-0000-0000-000000000000',
+      tag_name: 'Recursividad',
+      tag_description: 'Retos que implican resolver problemas mediante funciones que se llaman a sí mismas.'
+    },
+    {
+      id_tag: '04104104-1041-0410-4104-104104104104',
+      tag_name: 'Algoritmos',
+      tag_description: 'Ejercicios centrados en diseño y optimización de algoritmos clásicos y personalizados.'
+    },
+    {
+      id_tag: '08208208-2082-0820-8208-208208208208',
+      tag_name: 'Estructuras',
+      tag_description: 'Retos sobre listas, pilas, colas, árboles, grafos y otras estructuras de datos.'
+    },
+    {
+      id_tag: '0c30c30c-30c3-0c30-c30c-30c30c30c30c',
+      tag_name: 'POO',
+      tag_description: 'Desafíos enfocados en Programación Orientada a Objetos: clases, herencia, polimorfismo, etc.'
+    }
+  ]
+}
+
+export const javaTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: '11111111-1111-1111-1111-111111111111',
+      tag_name: 'Spring',
+      tag_description: 'Retos utilizando el framework Spring y Spring Boot.'
+    },
+    {
+      id_tag: '22222222-2222-2222-2222-222222222222',
+      tag_name: 'Collections',
+      tag_description: 'Ejercicios sobre el uso de ArrayList, HashSet, HashMap y otras colecciones.'
+    },
+    {
+      id_tag: '33333333-3333-3333-3333-333333333333',
+      tag_name: 'Threads',
+      tag_description: 'Desafíos de programación concurrente y multihilos.'
+    },
+    {
+      id_tag: '44444444-4444-4444-4444-444444444444',
+      tag_name: 'Interfaces',
+      tag_description: 'Retos sobre implementación y uso de interfaces en Java.'
+    }
+  ]
+}
+
+export const javascriptTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
+      tag_name: 'Promises',
+      tag_description: 'Retos sobre programación asíncrona y manejo de promesas.'
+    },
+    {
+      id_tag: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
+      tag_name: 'DOM',
+      tag_description: 'Ejercicios de manipulación del Document Object Model.'
+    },
+    {
+      id_tag: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
+      tag_name: 'Arrays',
+      tag_description: 'Desafíos utilizando métodos de array como map, filter, reduce.'
+    },
+    {
+      id_tag: 'dddddddd-dddd-dddd-dddd-dddddddddddd',
+      tag_name: 'Closures',
+      tag_description: 'Retos sobre scope y closures en JavaScript.'
+    }
+  ]
+}
+
+export const sqlTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: '55555555-5555-5555-5555-555555555555',
+      tag_name: 'Joins',
+      tag_description: 'Ejercicios de consultas con diferentes tipos de JOIN.'
+    },
+    {
+      id_tag: '66666666-6666-6666-6666-666666666666',
+      tag_name: 'Subqueries',
+      tag_description: 'Retos utilizando subconsultas y consultas anidadas.'
+    },
+    {
+      id_tag: '77777777-7777-7777-7777-777777777777',
+      tag_name: 'Agregación',
+      tag_description: 'Desafíos con funciones de agregación como COUNT, SUM, AVG.'
+    },
+    {
+      id_tag: '88888888-8888-8888-8888-888888888888',
+      tag_name: 'Triggers',
+      tag_description: 'Ejercicios sobre creación y uso de triggers en SQL.'
+    }
+  ]
+}
+
+export const pythonTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: '99999999-9999-9999-9999-999999999999',
+      tag_name: 'Django',
+      tag_description: 'Retos utilizando el framework Django.'
+    },
+    {
+      id_tag: 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
+      tag_name: 'List Comprehension',
+      tag_description: 'Ejercicios usando comprensión de listas y generadores.'
+    },
+    {
+      id_tag: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
+      tag_name: 'Decorators',
+      tag_description: 'Desafíos sobre creación y uso de decoradores en Python.'
+    },
+    {
+      id_tag: '12121212-1212-1212-1212-121212121212',
+      tag_name: 'NumPy',
+      tag_description: 'Retos de computación numérica con NumPy.'
+    }
+  ]
+}
+
+export const typescriptTags: TagResponse = {
+  offset: 0,
+  limit: 4,
+  count: 4,
+  results: [
+    {
+      id_tag: '34343434-3434-3434-3434-343434343434',
+      tag_name: 'Interfaces',
+      tag_description: 'Retos sobre definición y uso de interfaces en TypeScript.'
+    },
+    {
+      id_tag: '56565656-5656-5656-5656-565656565656',
+      tag_name: 'Generics',
+      tag_description: 'Ejercicios utilizando tipos genéricos.'
+    },
+    {
+      id_tag: '78787878-7878-7878-7878-787878787878',
+      tag_name: 'Decorators',
+      tag_description: 'Desafíos sobre implementación de decoradores en TypeScript.'
+    },
+    {
+      id_tag: '90909090-9090-9090-9090-909090909090',
+      tag_name: 'Type Guards',
+      tag_description: 'Retos sobre el uso de guardas de tipo y narrowing.'
+    }
+  ]
+}


Reviews:
user: login: GuillemBo
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T09:03:19Z
user: login: GuillemBo
body: The code is functioning as expected, and the requested changes have been implemented.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-31T10:08:21Z
user: login: ivilarop
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T09:35:35Z
user: login: mcallejo-10
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T10:38:53Z
user: login: ivilarop
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T08:01:45Z
user: login: mcallejo-10
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T09:47:37Z
user: login: mcallejo-10
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T09:48:09Z
user: login: mcallejo-10
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:57:52Z
user: login: mcallejo-10
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:58:16Z
user: login: ivilarop
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T11:40:25Z

Review comments:
diff_hunk: @@ -16,6 +16,9 @@ import { java } from '@codemirror/lang-java'
 import { python } from '@codemirror/lang-python'
 import { basicSetup } from 'codemirror'
 
+// Añadir el import al inicio del archivo
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: You can delete this comment
created_at: 2025-03-31T08:28:51Z
author_association: COLLABORATOR
diff_hunk: @@ -136,7 +143,7 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
         this.languages = results || []
         console.log('Idiomas cargados:', this.languages) // Lista completa de idiomas
-        console.log(JSON.stringify(this.languages, null, 2))
+        // console.log(JSON.stringify(this.languages, null, 2))
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: You can delete this comment since is not doing anything
created_at: 2025-03-31T08:31:04Z
author_association: COLLABORATOR
diff_hunk: @@ -136,7 +143,7 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: You can delete this comment
created_at: 2025-03-31T08:31:20Z
author_association: COLLABORATOR
diff_hunk: @@ -168,6 +176,21 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     }
   }
 
+  private loadTagsForLanguage (language: string): void {
+    const tagMap = {
+      PHP: phpTags,
+      Java: javaTags,
+      Javascript: javascriptTags,
+      SQL: sqlTags,
+      Python: pythonTags,
+      Typescript: typescriptTags
+    }
+
+    const selectedLanguageTags = tagMap[language as keyof typeof tagMap]
+    this.currentTags = selectedLanguageTags?.results ?? []
+    this.selectedTags = [] // Resetear tags seleccionados al cambiar de lenguaje
+  }
+
   // Validación del formulario (código original)
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: The code is already self-explanatory. You can delete the comment
created_at: 2025-03-31T08:42:02Z
author_association: COLLABORATOR
diff_hunk: @@ -147,8 +154,9 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
 
   // Nuevo método para manejar el cambio de lenguaje
   // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-  onLanguageChange (language: string) {
+  onLanguageChange (language: string): void {
     this.challenge.language = language
+    this.loadTagsForLanguage(language)
     /* istanbul ignore next */
     // Actualiza CodeMirror con el nuevo lenguaje
     if (this.editor != null) {
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: You can clear the comments that have no use
created_at: 2025-03-31T08:42:57Z
author_association: COLLABORATOR
diff_hunk: @@ -168,6 +176,21 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     }
   }
 
+  private loadTagsForLanguage (language: string): void {
+    const tagMap = {
+      PHP: phpTags,
+      Java: javaTags,
+      Javascript: javascriptTags,
+      SQL: sqlTags,
+      Python: pythonTags,
+      Typescript: typescriptTags
+    }
+
+    const selectedLanguageTags = tagMap[language as keyof typeof tagMap]
+    this.currentTags = selectedLanguageTags?.results ?? []
+    this.selectedTags = [] // Resetear tags seleccionados al cambiar de lenguaje
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: GuillemBo
body: you can clear the comment. 
created_at: 2025-03-31T08:55:41Z
author_association: COLLABORATOR
diff_hunk: @@ -205,4 +227,18 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
       }
     })
   }
+
+  onTagSelect (tag: string): void {
+    const index = this.selectedTags.indexOf(tag)
+    if (index === -1) {
+      this.selectedTags.push(tag)
+    } else {
+      this.selectedTags.splice(index, 1)
+    }
+    console.log('Tags seleccionados:', this.selectedTags)
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: ivilarop
body: delete it
created_at: 2025-04-01T09:27:02Z
author_association: COLLABORATOR
diff_hunk: @@ -136,7 +143,6 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
         this.languages = results || []
         console.log('Idiomas cargados:', this.languages) // Lista completa de idiomas
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: ivilarop
body: delete it
created_at: 2025-04-01T09:27:47Z
author_association: COLLABORATOR
diff_hunk: @@ -11,4 +11,6 @@ export interface CreateChallenge {
   level: 'EASY' | 'MEDIUM' | 'HARD'
   language: string
   solution: string
+  topic: string
path: src/app/models/create-challenge.interface.ts
user: login: ivilarop
body: what is it?
created_at: 2025-04-01T09:34:08Z
author_association: COLLABORATOR
diff_hunk: @@ -11,4 +11,6 @@ export interface CreateChallenge {
   level: 'EASY' | 'MEDIUM' | 'HARD'
   language: string
   solution: string
+  topic: string
path: src/app/models/create-challenge.interface.ts
user: login: mcallejo-10
body: Update to match back call, Albert sent me this json: 

{
  "challengeTitle": "Test with tags",
  "description": "string",
  "level": "EASY",
  "language": "Java",
  "solution": "string",
  "topic": "ALL",
  "tags": [
    "Recursividad"
  ]
}
created_at: 2025-04-01T10:38:53Z
author_association: COLLABORATOR
diff_hunk: @@ -11,4 +11,6 @@ export interface CreateChallenge {
   level: 'EASY' | 'MEDIUM' | 'HARD'
   language: string
   solution: string
+  topic: string
path: src/app/models/create-challenge.interface.ts
user: login: ivilarop
body: Yes, I understand, but the concept of a topic doesn't apply to this change. I understand that we have to include it to regularize it with the backend, but it creates confusion because it seems like the topic functionality is being implemented. If it doesn't give an error without a topic, let's not include it.
created_at: 2025-04-02T07:48:51Z
author_association: COLLABORATOR
diff_hunk: @@ -11,4 +11,6 @@ export interface CreateChallenge {
   level: 'EASY' | 'MEDIUM' | 'HARD'
   language: string
   solution: string
+  topic: string
path: src/app/models/create-challenge.interface.ts
user: login: ivilarop
body: We've been discussing this with Albert, and we believe the best and most optimal option is to send the tag's UUIDs. This way, we don't force the backend to search the database (TagDocument) for the UUIDs before saving them to ChallengeDocument.

"tags": [
  "67e7e9938a177b9eec207e0a"  // the id_tag ​​of the "Recursion" document
]
created_at: 2025-04-02T07:52:49Z
author_association: COLLABORATOR
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts
user: login: ivilarop
body: Does adding the tag id affect the tests?
created_at: 2025-04-02T07:53:42Z
author_association: COLLABORATOR
diff_hunk: @@ -128,15 +135,12 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     return extensions[language as keyof typeof extensions] || javascript
   }
 
-  // Carga los lenguajes del backend
-  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
   loadLanguages (): void {
     this.challengeFormService.getAllLangugesCreateForm().subscribe({
       next: ({ results }) => {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
         this.languages = results || []
         console.log('Idiomas cargados:', this.languages) // Lista completa de idiomas
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: ivilarop
body: must be deleted the console.log. Although it is a historic change, we can take advantage of it to clean up since it is a small change.
created_at: 2025-04-02T07:55:01Z
author_association: COLLABORATOR
diff_hunk: @@ -144,6 +144,23 @@ <h4><span class="section-number">3</span> Información adicional</h4>
               </div>
             </div>
           </div>
+          @if (challenge.language) {
+            <div class="form-group mb-4">
+              <label class="form-label">Tags</label>
+              <div class="tags-group">
+                @for (tag of currentTags; track tag.id_tag) {
+                  <button 
+                    type="button" 
+                    class="tag-button"
+                    [class.active]="isTagSelected(tag.tag_name)"
+                    (click)="onTagSelect(tag.tag_name)"
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.html
user: login: ivilarop
body: change for id tag
created_at: 2025-04-02T07:56:53Z
author_association: COLLABORATOR
diff_hunk: @@ -33,10 +36,14 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     description: '',
     level: 'EASY',
     language: '' as string,
-    solution: ''
+    solution: '',
+    topic: '',
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: ivilarop
body: As we discussed earlier, if removing the topic doesn't give an error, don't include it. If we want to include it in the future, we'll make the change.
created_at: 2025-04-02T07:59:50Z
author_association: COLLABORATOR
diff_hunk: @@ -33,10 +36,14 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     description: '',
     level: 'EASY',
     language: '' as string,
-    solution: ''
+    solution: '',
+    topic: '',
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: mcallejo-10
body: I've removed the word "topic" from tags and verified that all functionality continues to work properly.
created_at: 2025-04-02T09:47:37Z
author_association: COLLABORATOR
diff_hunk: @@ -128,15 +135,12 @@ export class ChallengeFormComponent implements AfterViewInit, OnDestroy {
     return extensions[language as keyof typeof extensions] || javascript
   }
 
-  // Carga los lenguajes del backend
-  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
   loadLanguages (): void {
     this.challengeFormService.getAllLangugesCreateForm().subscribe({
       next: ({ results }) => {
         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
         this.languages = results || []
         console.log('Idiomas cargados:', this.languages) // Lista completa de idiomas
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.ts
user: login: mcallejo-10
body: deleted
created_at: 2025-04-02T09:48:09Z
author_association: COLLABORATOR
diff_hunk: @@ -144,6 +144,23 @@ <h4><span class="section-number">3</span> Información adicional</h4>
               </div>
             </div>
           </div>
+          @if (challenge.language) {
+            <div class="form-group mb-4">
+              <label class="form-label">Tags</label>
+              <div class="tags-group">
+                @for (tag of currentTags; track tag.id_tag) {
+                  <button 
+                    type="button" 
+                    class="tag-button"
+                    [class.active]="isTagSelected(tag.tag_name)"
+                    (click)="onTagSelect(tag.tag_name)"
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.html
user: login: mcallejo-10
body: changed 
created_at: 2025-04-02T10:57:52Z
author_association: COLLABORATOR
path: src/app/modules/challenge/components/challenge-form/challenge-form.component.spec.ts
user: login: mcallejo-10
body: tests updated
created_at: 2025-04-02T10:58:16Z
author_association: COLLABORATOR

PR comments:
user: login: mcallejo-10
created_at: 2025-03-31T09:43:00Z
body: - Added comprehensive test suite for tag management functionality

- Removed redundant comments
author_association: COLLABORATOR
user: login: mcallejo-10
created_at: 2025-04-02T09:52:39Z
body: ## Changes Made
- Updated tag selection functionality to use UUIDs instead of name
- Updated  tag selection and deselection functionality
- Implemented tag management with proper data structure
- Updated test mocks to match real data format
- Fix test coverage for tag management
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T12:16:26Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=IT-Academy-BCN_ita-challenges-frontend&pullRequest=563) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=IT-Academy-BCN_ita-challenges-frontend&pullRequest=563&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=IT-Academy-BCN_ita-challenges-frontend&pullRequest=563)


author_association: NONE