Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### [ita-challenges-frontend-3.35.0-UNRELEASED] - 2026-03-07

### Added
- Add delete challenge functionality with confirmation and success modals.

### [ita-challenges-frontend-3.34.0-RELEASE] - 2026-03-07

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,16 @@ <h4><span class="section-number">3</span> {{ "modules.challenge.challengeForm.ad
}
</div>
</form>
<!-- Danger Zone -->
<div *ngIf="isEditMode" class="section">
<h4><span class="section-number">!</span> {{ "modules.challenge.challengeForm.dangerZone" | translate }}</h4>
<div class="danger-zone">
<span>
<h5>{{ "modules.challenge.challengeForm.deleteThisChallenge" | translate }}</h5>
<p>{{ "modules.challenge.challengeForm.deleteChallengeMsg" | translate }}</p>
</span>
<button class="delete-button">{{ "modules.challenge.challengeForm.deleteChallenge" | translate }}</button>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

@import 'src/styles/_colors.scss';

:host {
display: block;
Expand Down Expand Up @@ -52,11 +52,6 @@
padding-bottom: 0.5rem;
border-bottom: 1px solid #eee;
}
.section:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}

.section h4 {
display: flex;
Expand Down Expand Up @@ -336,4 +331,34 @@
display: inline-flex;
align-items: center;
margin-bottom: 0.75rem;
}
}

.danger-zone {
padding: 1rem;
display: flex;
color: $danger;
justify-content: space-between;
align-items: center;
border: 1px solid $danger;
border-radius: 0.5rem;

p {
margin: 0;
color: #333;
}

& button {
color: $danger;
border: 1px solid $danger;
background: rgba($danger, 0.1);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background: darken($danger, 10%);
color: white;
}
}
}
2 changes: 1 addition & 1 deletion src/app/services/challenge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/semi */
import { Inject, Injectable, inject, signal } from '@angular/core'
import { Observable, catchError, BehaviorSubject, of, throwError, forkJoin, switchMap } from 'rxjs'
import { delay, map, tap } from 'rxjs/operators'
import { map, tap } from 'rxjs/operators'
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'
import { type Itinerary } from '../models/itinerary.interface'
import { environment } from 'src/environments/environment'
Expand Down
51 changes: 50 additions & 1 deletion src/app/services/common-modal.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TestBed } from "@angular/core/testing";
import { TranslateService } from "@ngx-translate/core";
import Swal, { SweetAlertOptions } from "sweetalert2";
import { CommonModalService } from "./common-modal.service";

Expand All @@ -8,12 +9,19 @@ jest.mock("sweetalert2", () => ({
close: jest.fn(),
}));

const mockTranslateService = {
instant: jest.fn((key: string) => key),
};

describe("CommonModalService", () => {
let service: CommonModalService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [CommonModalService],
providers: [
CommonModalService,
{ provide: TranslateService, useValue: mockTranslateService },
],
});
service = TestBed.inject(CommonModalService);
});
Expand Down Expand Up @@ -82,4 +90,45 @@ describe("CommonModalService", () => {
expect(opts.text).toBe(errorMsg);
expect(opts.confirmButtonText).toBe("Volver");
});

it("deleteConfirmationModal should call Swal.fire with warning and cancel button", async () => {
await service.deleteConfirmationModal();

expect(Swal.fire).toHaveBeenCalledTimes(1);
const opts: SweetAlertOptions = (Swal.fire as jest.Mock).mock.calls[0][0];

expect(opts.icon).toBe("warning");
expect(opts.title).toBe("modules.modals.solution.title");
expect(opts.text).toBe("modules.challenge.challengeForm.deleteConfirmMsg");
expect(opts.showCancelButton).toBe(true);
expect(opts.confirmButtonText).toBe("modules.challenge.challengeForm.deleteConfirmBtn");
expect(opts.cancelButtonText).toBe("modules.modals.solution.btn-cancel");
expect(opts.customClass?.confirmButton).toBe("custom-danger-button");
expect(opts.customClass?.cancelButton).toBe("custom-cancel-button");
});

it("deleteSuccessModal should call Swal.fire with success options", async () => {
await service.deleteSuccessModal();

expect(Swal.fire).toHaveBeenCalledTimes(1);
const opts: SweetAlertOptions = (Swal.fire as jest.Mock).mock.calls[0][0];

expect(opts.icon).toBe("success");
expect(opts.title).toBe("modules.challenge.challengeForm.deleteSuccessTitle");
expect(opts.text).toBe("modules.challenge.challengeForm.deleteSuccessMsg");
expect(opts.customClass?.confirmButton).toBe("custom-danger-button");
});

it("deleteErrorModal should call Swal.fire with error options and custom message", async () => {
const errorMsg = "Something went wrong";
await service.deleteErrorModal(errorMsg);

expect(Swal.fire).toHaveBeenCalledTimes(1);
const opts: SweetAlertOptions = (Swal.fire as jest.Mock).mock.calls[0][0];

expect(opts.icon).toBe("error");
expect(opts.title).toBe("modules.challenge.challengeForm.deleteErrorTitle");
expect(opts.text).toBe(errorMsg);
expect(opts.customClass?.confirmButton).toBe("custom-danger-button");
});
});
63 changes: 61 additions & 2 deletions src/app/services/common-modal.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from "@angular/core";
import { inject, Injectable } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import Swal, { SweetAlertOptions, SweetAlertResult } from "sweetalert2";

@Injectable({
providedIn: "root",
})
export class CommonModalService {
constructor() {}
private readonly translate = inject(TranslateService);

loginRequestModal() {
const options: SweetAlertOptions = {
Expand Down Expand Up @@ -76,4 +77,62 @@ export class CommonModalService {
};
return Swal.fire(options);
}

deleteConfirmationModal(): Promise<SweetAlertResult> {
const prefix = "modules.challenge.challengeForm";
const options: SweetAlertOptions = {
icon: "warning",
title: this.translate.instant("modules.modals.solution.title"),
text: this.translate.instant(`${prefix}.deleteConfirmMsg`),
showCancelButton: true,
confirmButtonText: this.translate.instant(`${prefix}.deleteConfirmBtn`),
cancelButtonText: this.translate.instant("modules.modals.solution.btn-cancel"),
showCloseButton: true,
allowOutsideClick: true,
customClass: {
popup: "custom-modal",
title: "custom-title",
htmlContainer: "custom-html-container",
confirmButton: "custom-danger-button",
cancelButton: "custom-cancel-button",
},
};
return Swal.fire(options);
}

deleteSuccessModal(): Promise<SweetAlertResult> {
const prefix = "modules.challenge.challengeForm";
const options: SweetAlertOptions = {
icon: "success",
title: this.translate.instant(`${prefix}.deleteSuccessTitle`),
text: this.translate.instant(`${prefix}.deleteSuccessMsg`),
showCloseButton: true,
allowOutsideClick: true,
customClass: {
popup: "custom-modal",
title: "custom-title",
htmlContainer: "custom-html-container",
confirmButton: "custom-danger-button",
},
};
return Swal.fire(options);
}

deleteErrorModal(errorMessage: string): Promise<SweetAlertResult> {
const prefix = "modules.challenge.challengeForm";
const options: SweetAlertOptions = {
icon: "error",
title: this.translate.instant(`${prefix}.deleteErrorTitle`),
text: errorMessage,
showCloseButton: true,
allowOutsideClick: true,
customClass: {
popup: "custom-modal",
title: "custom-title",
htmlContainer: "custom-html-container",
confirmButton: "custom-danger-button",
},
};
return Swal.fire(options);
}
}
11 changes: 10 additions & 1 deletion src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@
"medium": "Mitjana",
"hard": "Difícil",
"tags": "Etiquetes",
"tagsRequired": "Selecciona almenys una etiqueta"
"tagsRequired": "Selecciona almenys una etiqueta",
"dangerZone": "Zona de perill",
"deleteChallenge": "Eliminar repte",
"deleteThisChallenge": "Eliminar aquest repte",
"deleteChallengeMsg": "L'eliminació d'aquest repte és permanent i no es pot desfer.",
"deleteConfirmMsg": "Aquesta acció no es pot desfer. El repte serà eliminat permanentment.",
"deleteConfirmBtn": "Sí, eliminar",
"deleteSuccessTitle": "Repte eliminat",
"deleteSuccessMsg": "El repte s'ha eliminat correctament.",
"deleteErrorTitle": "Error en eliminar el repte"
},
"bookmarksView":{
"bookmarksSaved": "Reptes desats",
Expand Down
11 changes: 10 additions & 1 deletion src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@
"medium": "Medium",
"hard": "Hard",
"tags": "Tags",
"tagsRequired": "Select at least one tag"
"tagsRequired": "Select at least one tag",
"dangerZone": "Danger zone",
"deleteChallenge": "Delete challenge",
"deleteThisChallenge": "Delete this challenge",
"deleteChallengeMsg": "Deleting this challenge is permanent and cannot be undone.",
"deleteConfirmMsg": "This action cannot be undone. The challenge will be permanently deleted.",
"deleteConfirmBtn": "Yes, delete",
"deleteSuccessTitle": "Challenge deleted",
"deleteSuccessMsg": "The challenge has been successfully deleted.",
"deleteErrorTitle": "Error deleting challenge"
},
"bookmarksView":{
"bookmarksSaved": "Saved challenges",
Expand Down
11 changes: 10 additions & 1 deletion src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@
"medium": "Media",
"hard": "Difícil",
"tags": "Tags",
"tagsRequired": "Selecciona al menos una etiqueta"
"tagsRequired": "Selecciona al menos una etiqueta",
"dangerZone": "Zona de peligro",
"deleteChallenge": "Eliminar reto",
"deleteThisChallenge": "Eliminar este reto",
"deleteChallengeMsg": "Eliminar este reto es permanente y no se puede deshacer.",
"deleteConfirmMsg": "Esta acción no se puede deshacer. El reto será eliminado permanentemente.",
"deleteConfirmBtn": "Sí, eliminar",
"deleteSuccessTitle": "Reto eliminado",
"deleteSuccessMsg": "El reto ha sido eliminado correctamente.",
"deleteErrorTitle": "Error al eliminar el reto"
},
"bookmarksView":{
"bookmarksSaved": "Retos guardados",
Expand Down
14 changes: 14 additions & 0 deletions src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ a {
}
}

.custom-danger-button {
background-color: $danger;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 1rem;
cursor: pointer;

&:hover {
background-color: darken($danger, 10%);
}
}

.custom-cancel-button {
background-color: #e0e0e0;
color: #333333;
Expand Down
Loading