Skip to content

Commit e4c1eda

Browse files
authored
Merge pull request #753 from IT-Academy-BCN/feature/#115-update-challenge-card-element
Feature/#115 update challenge card element
2 parents bdeb060 + 3642291 commit e4c1eda

23 files changed

+492
-347
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to
55
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
### [ita-challenges-frontend-3.22.0-RELEASE] - 2026-02-11
8+
9+
### Changed
10+
- Changed the challenge card layout and style to fit the new design
11+
712
### [ita-challenges-frontend-3.21.0-RELEASE] - 2026-02-12
813

914
### Added

conf/.env.CI.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
MICROSERVICE_DEPLOY=ita-challenges-frontend
2-
MICROSERVICE_VERSION=3.21.0-RELEASE
2+
MICROSERVICE_VERSION=3.22.0-RELEASE

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ita-challenges-frontend",
3-
"version": "3.21.0-RELEASE",
3+
"version": "3.22.0-RELEASE",
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve --proxy-config proxy.conf.dev.json",

src/app/core/layout/main/main.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757
width: 100%;
5858
height: calc(100% - 20px) !important;
5959
padding: 0px 40px 20px 40px;
60+
background-color: $gray-4;
61+
overflow-y: scroll;
62+
display: flex;
63+
flex-direction: column;
6064
}
6165

6266
app-main-menu {

src/app/modules/starter/components/starter/starter.component.html

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,23 @@ <h2>{{ "modules.starter.main.section2.title" | translate }}</h2>
5252

5353
<main id="challenges-container" class="col-12 col-xl-8">
5454

55-
<div class="d-flex flex-column gap-3" id="challenges" #challenge>
55+
<div id="challenges" #challenge>
5656
<ng-container *ngIf="(challenges?.length || 0) > 0; else noResultsTpl">
57-
<app-challenge-card *ngFor="let challenge of challenges; let i = index"
58-
[title]="challenge.challenge_title | dynamicTranslate" [creation_date]="challenge.creation_date"
59-
[level]="challenge.level" [popularity]="challenge.popularity" [languages]="challenge.languages"
60-
[id]="challenge.id_challenge" [favorites_count]="challenge.timesFavorite || 0"
57+
<app-challenge-card
58+
*ngFor="let challenge of challenges; let i = index"
59+
[title]="challenge.challenge_title | dynamicTranslate"
60+
[description]="challenge.detail.description | dynamicTranslate"
61+
[languages]="challenge.languages"
62+
[creation_date]="challenge.creation_date"
63+
[level]="challenge.level"
64+
[popularity]="challenge.popularity"
65+
[id]="challenge.id_challenge"
66+
[favorites_count]="challenge.timesFavorite || 0"
6167
[challenge_timesSolved]="challenge.timesSolved || timesSolved"
6268
[isFavorite]="isFavoriteChallenge(challenge.id_challenge)"
6369
[isBookmarked]="isBookmarkedChallenge(challenge.id_challenge)"
64-
[solutionStatus]="solutionStatusMap[challenge.id_challenge]">
70+
[solutionStatus]="solutionStatusMap[challenge.id_challenge]"
71+
>
6572
</app-challenge-card>
6673
</ng-container>
6774
</div>

src/app/modules/starter/components/starter/starter.component.scss

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
.starter {
66
display: flex;
77
flex-direction: column;
8-
height: 100%;
98
width: 100%;
10-
margin-top: 1rem;
9+
padding: 2rem;
1110
}
1211

1312
h2 {
@@ -92,34 +91,22 @@ app-starter-filters p {
9291
}
9392
}
9493

95-
/* On desktop, let the filters column shrink to content instead of fixed grid width */
96-
@media (min-width: 1200px) {
97-
#filters-container.col-3 {
98-
flex: 0 0 auto; /* don't force a 25% column width */
99-
width: auto; /* shrink-wrap to its contents */
100-
max-width: 100%;
101-
margin-right: 20px; /* add spacing between filters and challenges */
102-
}
103-
/* Let challenges area take the remaining space on xl and above */
104-
#challenges-container.col-xl-8 {
105-
flex: 1 1 auto; /* grow to fill leftover width */
106-
width: auto; /* unset fixed 8/12 width from grid */
107-
max-width: 100%;
108-
}
109-
}
110-
11194
#challenges-container {
11295
height: 100%;
96+
width: 100%;
11397
overflow: hidden;
11498
display: flex;
11599
flex-direction: column;
100+
padding-top: 2rem;
116101
}
117102

118103
#challenges {
119-
flex: 1;
104+
padding-top: 1rem;
120105
overflow-y: auto;
121-
scrollbar-width: none;
122-
-ms-overflow-style: none;
106+
width: 100%;
107+
display: grid;
108+
grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
109+
gap: 1rem;
123110
}
124111

125112
#challenges::-webkit-scrollbar {

src/app/services/challenge.service.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,4 +362,63 @@ describe('ChallengeService', () => {
362362
req.flush(null, mockError);
363363
});
364364
});
365-
})
365+
366+
describe('getChallengeTags', () => {
367+
const mockChallengeId = '5caa6142-a49e-4415-a8fc-439669777d1d';
368+
const mockTagResponse = {
369+
offset: 0,
370+
limit: 3,
371+
count: 3,
372+
results: [
373+
{ id_tag: 'tag-1', tag_name: 'Arrays', tag_description: 'Array challenges' },
374+
{ id_tag: 'tag-2', tag_name: 'Loops', tag_description: 'Loop challenges' },
375+
{ id_tag: 'tag-3', tag_name: 'Logic', tag_description: 'Logic challenges' }
376+
]
377+
};
378+
379+
it('should fetch challenge tags successfully', (done) => {
380+
service.getChallengeTags(mockChallengeId).subscribe(response => {
381+
expect(response).toEqual(mockTagResponse);
382+
expect(response.results.length).toBe(3);
383+
done();
384+
});
385+
386+
const req = httpMock.expectOne(
387+
`${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_ITA_CHALLENGE_TAGS}/${mockChallengeId}`
388+
);
389+
expect(req.request.method).toBe('GET');
390+
expect(req.request.headers.get('Content-Type')).toBe('application/json');
391+
req.flush(mockTagResponse);
392+
});
393+
394+
it('should handle empty results', (done) => {
395+
const emptyResponse = { offset: 0, limit: 0, count: 0, results: [] };
396+
397+
service.getChallengeTags(mockChallengeId).subscribe(response => {
398+
expect(response.results).toEqual([]);
399+
expect(response.count).toBe(0);
400+
done();
401+
});
402+
403+
const req = httpMock.expectOne(
404+
`${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_ITA_CHALLENGE_TAGS}/${mockChallengeId}`
405+
);
406+
req.flush(emptyResponse);
407+
});
408+
409+
it('should handle HTTP errors', (done) => {
410+
service.getChallengeTags(mockChallengeId).subscribe({
411+
next: () => fail('should have failed with 404 error'),
412+
error: (error) => {
413+
expect(error.status).toBe(404);
414+
done();
415+
}
416+
});
417+
418+
const req = httpMock.expectOne(
419+
`${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_ITA_CHALLENGE_TAGS}/${mockChallengeId}`
420+
);
421+
req.flush(null, { status: 404, statusText: 'Not Found' });
422+
});
423+
});
424+
})

src/app/services/challenge.service.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { environment } from 'src/environments/environment'
99
import { type Challenge } from '../models/challenge.model'
1010
import { type Language } from '../models/language.model'
1111
import { type FavoriteResponse } from '../models/favorite-response.interface'
12+
import { type TagResponse } from '../models/tag-response.interface'
1213
import { type CreateChallenge } from '../models/create-challenge.interface'
1314
import { CookieService } from 'ngx-cookie-service'
1415
import { AuthService } from './auth.service'
@@ -202,32 +203,41 @@ export class ChallengeService {
202203

203204

204205
getRelatedChallenges(challengeId: string): Observable<Challenge[]> {
205-
const headers = {
206-
'Content-Type': 'application/json',
207-
...this.authService.getAuthHeaders()
208-
};
209-
210-
const url = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}/challenge/challenges/${challengeId}/related`;
211-
212-
return this.http.get<{results: Challenge[] }>(url, { headers }).pipe(
213-
map(response => response.results),
214-
catchError((error: HttpErrorResponse) => {
215-
console.error('Error fetching related challenges:', error);
216-
return throwError(() => error);
217-
})
218-
);
219-
}
206+
const headers = {
207+
'Content-Type': 'application/json',
208+
...this.authService.getAuthHeaders()
209+
};
220210

221-
editChallenge(challengeId: string, challenge: Partial<Challenge>): Observable<Challenge> {
222-
const url = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_EDIT_CHALLENGE_URL}/${challengeId}/update`;
211+
const url = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}/challenge/challenges/${challengeId}/related`;
223212

224-
const headers = {
225-
'Content-Type': 'application/json',
226-
...this.authService.getAuthHeaders()
227-
};
213+
return this.http.get<{results: Challenge[] }>(url, { headers }).pipe(
214+
map(response => response.results),
215+
catchError((error: HttpErrorResponse) => {
216+
console.error('Error fetching related challenges:', error);
217+
return throwError(() => error);
218+
})
219+
);
220+
}
228221

229-
return this.http.put<Challenge>(url, challenge, { headers });
230-
}
222+
editChallenge(challengeId: string, challenge: Partial<Challenge>): Observable<Challenge> {
223+
const url = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_EDIT_CHALLENGE_URL}/${challengeId}/update`;
231224

225+
const headers = {
226+
'Content-Type': 'application/json',
227+
...this.authService.getAuthHeaders()
228+
};
229+
230+
return this.http.put<Challenge>(url, challenge, { headers });
231+
}
232+
233+
getChallengeTags(challengeId: string): Observable<TagResponse> {
234+
const url = `${environment.BACKEND_ITA_CHALLENGE_BASE_URL}${environment.BACKEND_ITA_CHALLENGE_TAGS}/${challengeId}`
235+
236+
const headers = {
237+
'Content-Type': 'application/json',
238+
};
239+
240+
return this.http.get<TagResponse>(url, { headers })
241+
}
232242

233243
}

src/app/services/starter.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { environment } from '../../environments/environment'
44
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
55
import { type FilterChallenge } from '../models/filter-challenge.model'
66
import { type Challenge, type ChallengeResponse } from '../models/challenge.model'
7+
import { type Tag, type TagResponse } from '../models/tag-response.interface'
8+
79
@Injectable({
810
providedIn: 'root'
911
})
12+
1013
export class StarterService {
1114
constructor (@Inject(HttpClient) private readonly http: HttpClient) {}
1215
cachedChallenges: ChallengeResponse | null = null
16+
cachedTags: TagResponse | null = null
1317

1418
private readonly refreshSubject = new Subject<void>()
1519
readonly refresh$ = this.refreshSubject.asObservable()
@@ -35,7 +39,7 @@ export class StarterService {
3539
}).pipe(
3640
tap((response) => {
3741
this.cachedChallenges = response
38-
}))
42+
}))
3943
}
4044

4145
getAllChallengesOffset (pageOffset: number, pageLimit: number): Observable<ChallengeResponse> {

0 commit comments

Comments
 (0)