diff --git a/cypress/e2e/report.cy.ts b/cypress/e2e/report.cy.ts index c7824b91..8a4bef4d 100644 --- a/cypress/e2e/report.cy.ts +++ b/cypress/e2e/report.cy.ts @@ -24,11 +24,8 @@ describe('Feature Report', () => { it('can\'t report with an empty reason', () => { loginAsVoluntary(); accessToReportModal(); - cy.get('p-button').contains('Envoyer') - .click(); - cy.get('app-textarea-field p') - .contains('Ce champ est requis') - .should('be.visible'); + cy.get('p-button[label="Envoyer"] button') + .should('be.disabled'); cy.get('body').find('.p-dialog-mask') .click({ force: true }); logout(); diff --git a/cypress/fixtures/common/userByToken.json b/cypress/fixtures/common/userByToken.json new file mode 100644 index 00000000..fd542811 --- /dev/null +++ b/cypress/fixtures/common/userByToken.json @@ -0,0 +1,5 @@ +{ + "user": { + + } +} \ No newline at end of file diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index ed8ab040..794b82a7 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,5 +1,6 @@ beforeEach(() => { // the home page's necessary mocks + cy.intercept('GET', '/auth/token', {fixture: null}).as('getUserByToken'); cy.intercept('GET', '/activities/future', { fixture: '/common/activities.json' }).as('getActivities'); cy.intercept('GET', '/themes', { fixture: '/common/themes.json' }).as('getThemes'); cy.intercept('GET', 'https://api.maptiler.com/maps/**', { statusCode: 200, body: {} }).as('mapTiler'); diff --git a/eslint.config.js b/eslint.config.js index 69cf778f..43fb6463 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -106,7 +106,6 @@ module.exports = tseslint.config( "@angular-eslint/template/no-call-expression": "warn", "@angular-eslint/template/no-distracting-elements": "warn", "@angular-eslint/template/mouse-events-have-key-events": "warn", - "@angular-eslint/template/no-inline-styles": "warn", "@angular-eslint/template/no-interpolation-in-attributes": "warn", "@angular-eslint/template/prefer-control-flow": "warn", }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d2dc9c73..f2aa2296 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -22,7 +22,7 @@ export class AppComponent implements OnInit { loading = false; ngOnInit(): void { - this._authFacade.initUserFromStorage(); + this._authFacade.getUserByToken(); this._handleRouterEvents(); this._applySavedTheme(); } diff --git a/src/app/common/components/header/header.component.html b/src/app/common/components/header/header.component.html index 3edd2f47..df1689f6 100644 --- a/src/app/common/components/header/header.component.html +++ b/src/app/common/components/header/header.component.html @@ -34,5 +34,5 @@

a l'asso !

- + diff --git a/src/app/common/components/header/header.component.scss b/src/app/common/components/header/header.component.scss index 2b16170a..9f3f4e1f 100644 --- a/src/app/common/components/header/header.component.scss +++ b/src/app/common/components/header/header.component.scss @@ -57,6 +57,10 @@ header { border: none; transition: background-color 0.3s ease; font-family: var(--font-family-p); + + @media screen and (max-width: 400px) { + padding: 0.75rem 1rem; + } } .inscription-btn { diff --git a/src/app/common/components/header/header.component.ts b/src/app/common/components/header/header.component.ts index 198dcdcd..b453f3b8 100644 --- a/src/app/common/components/header/header.component.ts +++ b/src/app/common/components/header/header.component.ts @@ -44,15 +44,21 @@ export class HeaderComponent extends DestroyableComponent implements OnInit { showRegisterModal = false; showLoginModal = false; + email: string = ''; + showPasswordForgottenModal = false; ButtonStyleClass = ButtonStyleClass; userInfos$: Observable = this._authFacade.getUserHeaderInfo(); ngOnInit(): void { - this._authService.openLoginModal$.pipe(this.untilDestroyed()).subscribe(() => { + this._authService.openLoginModal$.pipe(this.untilDestroyed()).subscribe((email: string | null) => { + this.email = email ?? ''; this.handleLoginModal(true); }); + this._authService.openSubscribeModal$.pipe(this.untilDestroyed()).subscribe(() => { + this.handleSubscribeModal(true); + }); } openRegisterModal(): void { @@ -62,6 +68,10 @@ export class HeaderComponent extends DestroyableComponent implements OnInit { handleLoginModal(status: boolean): void { this.showLoginModal = status; } + handleSubscribeModal(status: boolean): void { + this.showLoginModal = false; + this.showRegisterModal = status; + } closeLoginModal(): void { this.showLoginModal = false; diff --git a/src/app/common/components/input-field-error/input-field-error.component.html b/src/app/common/components/input-field-error/input-field-error.component.html index bb408559..b1d610c4 100644 --- a/src/app/common/components/input-field-error/input-field-error.component.html +++ b/src/app/common/components/input-field-error/input-field-error.component.html @@ -14,14 +14,17 @@ @if (control.errors?.['invalidAddress']) {

L'adresse sélectionnée est invalide. Veuillez en choisir une depuis la liste

} - @if (control.errors?.['dateRequired']) { -

La date saisie doit être dans le futur

+ @if (control.errors?.['pastDate']) { +

La date doit être dans le passé.

+ } + @if (control.errors?.['futureDate']) { +

La date doit être dans le futur.

} @if (control.errors?.['email']) {

Adresse e-mail invalide

} @if (control.errors?.['maxSize']) { -

Le fichier est trop volumineux (max 5MB).

+

Le fichier est trop volumineux (max 10MB).

} @if (control.errors?.['invalidFormat']) {

Format non autorisé.

diff --git a/src/app/common/components/input-field/input-field.component.html b/src/app/common/components/input-field/input-field.component.html index 20b8fcbb..ae0c935e 100644 --- a/src/app/common/components/input-field/input-field.component.html +++ b/src/app/common/components/input-field/input-field.component.html @@ -8,7 +8,9 @@ appendTo="body" dateFormat="dd.mm.yy" [ariaLabel]="fieldConfig.label" - [placeholder]="fieldConfig.placeholder ?? ''"> + [placeholder]="fieldConfig.placeholder ?? ''" + [minDate]="dateMode === 'future' ? minDate : null" + [maxDate]="dateMode === 'past' ? maxDate : null"> } @else if (fieldConfig.type === 'password') { + autocomplete="on" /> } diff --git a/src/app/common/components/input-field/input-field.component.ts b/src/app/common/components/input-field/input-field.component.ts index 65d99c79..ecdfed7b 100644 --- a/src/app/common/components/input-field/input-field.component.ts +++ b/src/app/common/components/input-field/input-field.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CalendarModule } from 'primeng/calendar'; import { DividerModule } from 'primeng/divider'; @@ -8,6 +8,7 @@ import { IftaLabelModule } from 'primeng/iftalabel'; import { InputTextModule } from 'primeng/inputtext'; import { PasswordModule } from 'primeng/password'; import { FormField } from 'src/app/features/authentication/models/form.model'; +import { updateDateLimits } from '../../utils/date.utils'; @Component({ selector: 'app-input-field', @@ -26,14 +27,25 @@ import { FormField } from 'src/app/features/authentication/models/form.model'; templateUrl: './input-field.component.html', styleUrls: ['./input-field.component.scss'], }) -export class InputFieldComponent { +export class InputFieldComponent implements OnInit { @Input() submitted: boolean = true; @Input() disabled: boolean = false; @Input() variant: 'in' | 'on' = 'in'; @Input() useIftaLabel: boolean = false; @Input() showPasswordRules: boolean = false; + @Input({ required: true }) fieldConfig!: FormField; @Input({ required: true }) formGroup: FormGroup; + @Input() dateMode: 'future' | 'past' | 'all' = 'all'; + + minDate: Date | undefined; + maxDate: Date | undefined; + + ngOnInit(): void { + const { maxDate, minDate } = updateDateLimits(this.dateMode); + this.maxDate = maxDate; + this.minDate = minDate; + } get inputId(): string { return 'input_' + this.fieldConfig.name; diff --git a/src/app/common/components/multiple-input-field/multiple-input-field.component.html b/src/app/common/components/multiple-input-field/multiple-input-field.component.html index 50d6e11a..a4ef3763 100644 --- a/src/app/common/components/multiple-input-field/multiple-input-field.component.html +++ b/src/app/common/components/multiple-input-field/multiple-input-field.component.html @@ -16,6 +16,8 @@ (onClear)="emitValues()" [inputId]="'input-' + i" [placeholder]="field.placeholder" + [minDate]="dateMode === 'future' ? minDate : null" + [maxDate]="dateMode === 'past' ? maxDate : null" [showIcon]="true" inputStyleClass="custom-calendar-input" dateFormat="dd/mm/yy" /> diff --git a/src/app/common/components/multiple-input-field/multiple-input-field.component.ts b/src/app/common/components/multiple-input-field/multiple-input-field.component.ts index 0ecea643..a0b6b7a6 100644 --- a/src/app/common/components/multiple-input-field/multiple-input-field.component.ts +++ b/src/app/common/components/multiple-input-field/multiple-input-field.component.ts @@ -10,6 +10,7 @@ import { FormField } from 'src/app/features/authentication/models/form.model'; import { AutosaveFieldComponent } from '../../directives/autosave-field.component'; import { SaveStatus } from '../../models/status'; import { InputFieldErrorComponent } from '../input-field-error/input-field-error.component'; +import { updateDateLimits } from '../../utils/date.utils'; @Component({ selector: 'app-multiple-input-field', @@ -32,6 +33,7 @@ export class MultipleInputFieldComponent extends AutosaveFieldComponent>(); @Output() save = new EventEmitter>(); + @Input() dateMode: 'future' | 'past' | 'all' = 'all'; @Input() fieldConfigs: FormField[] = []; @Input() showSearchButton = false; @Input() showSaveButton = false; @@ -39,6 +41,8 @@ export class MultipleInputFieldComponent extends AutosaveFieldComponent { detail = "Vous n'avez pas la permission d'accéder à cette ressource."; } else if (error.status === HttpStatusCode.Unauthorized) { const message = error?.error?.message; + console.error('Unauthorized error message from server:', message); const isTokenExpired = message === 'Token expired'; if (isTokenExpired) { @@ -42,7 +43,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { authService.triggerLoginModal(); } else { summary = 'Authentification'; - detail = message || 'Accès non autorisé.'; + detail = 'Accès non autorisé.'; severity = 'error'; } } diff --git a/src/app/common/pipes/date-iso-pipe.ts b/src/app/common/pipes/date-iso-pipe.ts new file mode 100644 index 00000000..e1e391f6 --- /dev/null +++ b/src/app/common/pipes/date-iso-pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'isoDate' }) +export class IsoDatePipe implements PipeTransform { + transform(value: string | Date | null): string { + if (!value) return ''; + const date = value instanceof Date ? value : new Date(value); + return date.toISOString().split('T')[0]; // format yyyy-MM-dd + } +} diff --git a/src/app/common/utils/date.utils.ts b/src/app/common/utils/date.utils.ts new file mode 100644 index 00000000..0d4bdd2e --- /dev/null +++ b/src/app/common/utils/date.utils.ts @@ -0,0 +1,22 @@ +type DateLimits = { + minDate: Date | undefined; + maxDate: Date | undefined; +}; + +export function updateDateLimits(dateMode: string): DateLimits { + const today = new Date(); + let minDate: Date | undefined; + let maxDate: Date | undefined; + + if (dateMode === 'future') { + minDate = today; + maxDate = undefined; + } else if (dateMode === 'past') { + maxDate = today; + minDate = undefined; + } else { + minDate = undefined; + maxDate = undefined; + } + return { minDate, maxDate }; +} diff --git a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.html b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.html index b2ab6834..56d85840 100644 --- a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.html +++ b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.html @@ -33,6 +33,8 @@ [appendTo]="'body'" styleClass="custom-dialog" contentStyleClass="custom-dialog-content"> +

Sélectionner une photo parmi vos précédentes activités ou télécharger une nouvelle image

+
@if (existingImages$ | async; as existingImages) { @for (pic of existingImages; track pic.id) { @@ -56,7 +58,7 @@ [type]="'button'" (buttonClicked)="loadMorePictures()" [styleClass]="ButtonStyleClass.defaultWhite" - label="Plus d'images" + label="Charger plus d'images précédentes" ariaLabel="button pour charger plus de photos" size="small"> diff --git a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.scss b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.scss index bd7cf05d..0f1b88bc 100644 --- a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.scss +++ b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.scss @@ -76,12 +76,10 @@ } } - .modal-container { - ::ng-deep .p-dialog-header .p-dialog-title { - font-family: var(--font-family-big-medium-title); - font-size: var(--font-size-medium-title); - font-weight: 400 !important; - } + .modal-container ::ng-deep .p-dialog-header .p-dialog-title { + font-family: var(--font-family-big-medium-title); + font-size: var(--font-size-medium-title); + font-weight: 400 !important; .existing-photos { display: flex; diff --git a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.ts b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.ts index 8b8db2e8..b6d4ee43 100644 --- a/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.ts +++ b/src/app/features/activity/components/activity-add-photo/activity-add-photo.component.ts @@ -117,9 +117,9 @@ export class ActivityAddPhotoComponent implements OnInit, OnChanges { return; } - const maxFileSize = 2 * 1024 * 1024; + const maxFileSize = 10 * 1024 * 1024; if (file.size > maxFileSize) { - showErrorToast(this._toast, 'Le fichier est trop volumineux (max 2MB).'); + showErrorToast(this._toast, 'Le fichier est trop volumineux (max 10MB).'); return; } diff --git a/src/app/features/activity/components/activity-card/activity-card.component.ts b/src/app/features/activity/components/activity-card/activity-card.component.ts index 31837168..ede77de1 100644 --- a/src/app/features/activity/components/activity-card/activity-card.component.ts +++ b/src/app/features/activity/components/activity-card/activity-card.component.ts @@ -10,6 +10,8 @@ import { Activity, Participant } from '../../models/activity.model'; import { ActivityFacadeService } from '../../services/activity-facade.service'; import { FavoriteHeartComponent } from '../favorite-heart/favorite-heart.component'; import { InscriptionBadgeComponent } from '../inscription-badge/inscription-badge.component'; +import { UUIDTypes } from 'uuid'; +import { AuthFacade } from '../../../authentication/services/auth-facade.service'; @Component({ selector: 'app-activity-card', @@ -23,19 +25,21 @@ export class ActivityCardComponent implements OnInit { @Output() editActivity = new EventEmitter(); @Input() activity!: Activity; - @Input() showDeleteButton: boolean = false; @Input() showEditButton: boolean = false; private _activityFacadeService: ActivityFacadeService = inject(ActivityFacadeService); private _authService = inject(AuthService); + private _authFacadeService = inject(AuthFacade); public isSavedActivity$: Observable; public voluntariesRegistered$: Observable; public apiUrl: string = environment.apiUrl; public associationLogoUrl!: string; + public showDeleteButton: boolean = false; isVoluntary$: Observable = this._authService.isVoluntaryUser(); isAssociation$: Observable = this._authService.isAssociationUser(); isAdmin$: Observable = this._authService.isAdminUser(); + connectedUserId$: Observable = this._authFacadeService.getConnectedUserId(); showFavorite$: Observable = combineLatest([this.isVoluntary$, this.isAdmin$]).pipe( map(([isVoluntary, isAdmin]) => isVoluntary && !isAdmin) ); @@ -44,6 +48,12 @@ export class ActivityCardComponent implements OnInit { this.isSavedActivity$ = this._activityFacadeService.getIsSavedActivity(this.activity.id); this.voluntariesRegistered$ = this._activityFacadeService.getVoluntariesRegisteredToAnActivity(this.activity.id); this._setAssociationLogoUrl(); + + combineLatest([this.isAssociation$, this.isAdmin$, this.connectedUserId$]).subscribe(([isAssociation, isAdmin, connectedUserId]) => { + if ((isAssociation && this.activity.association.id === connectedUserId) || isAdmin) { + this.showDeleteButton = true; + } + }); } onDeleteClick(event: MouseEvent): void { diff --git a/src/app/features/activity/components/activity-description/activity-description.component.html b/src/app/features/activity/components/activity-description/activity-description.component.html index 2235e246..1b5c5db1 100644 --- a/src/app/features/activity/components/activity-description/activity-description.component.html +++ b/src/app/features/activity/components/activity-description/activity-description.component.html @@ -26,17 +26,19 @@

{{ activity.title }}

}
- @if (isVoluntary$ | async) { + @if ((isAssociation$ | async) === false) { @if (canShowRegisterButton$ | async) { -
- - -
+ @if (isActivityUpcoming()) { +
+ + +
+ } } @else {

Cette activité est complète

} diff --git a/src/app/features/activity/components/activity-description/activity-description.component.ts b/src/app/features/activity/components/activity-description/activity-description.component.ts index 0ba5ae12..ff16e445 100644 --- a/src/app/features/activity/components/activity-description/activity-description.component.ts +++ b/src/app/features/activity/components/activity-description/activity-description.component.ts @@ -16,7 +16,7 @@ import { InscriptionBadgeComponent } from '../inscription-badge/inscription-badg @Component({ selector: 'app-activity-description', imports: [InscriptionBadgeComponent, FavoriteHeartComponent, DatePipe, ButtonModule, SingleButtonComponent, AsyncPipe], - providers: [ConfirmationService], + providers: [], templateUrl: './activity-description.component.html', styleUrl: './activity-description.component.scss', }) @@ -29,11 +29,13 @@ export class ActivityDescriptionComponent implements OnInit { ButtonStyleClass = ButtonStyleClass; public apiUrl: string = environment.apiUrl; + public today: Date = new Date(); public isRegisteredActivity$: Observable; public isSavedActivity$: Observable; public voluntariesRegistered$: Observable; isVoluntary$: Observable = this._authService.isVoluntaryUser(); + isAssociation$: Observable = this._authService.isAssociationUser(); isNotFull$: Observable; canShowRegisterButton$: Observable; @@ -47,19 +49,40 @@ export class ActivityDescriptionComponent implements OnInit { ); } + isActivityUpcoming(): boolean { + const activityDate = new Date(this.activity.date); + return activityDate >= this.today; + } + onRegisterClick(activity: Activity): void { - this.isRegisteredActivity$.pipe(take(TAKE_1)).subscribe(isRegistered => { - if (isRegistered) { - this._confirmationService.confirm({ - message: 'Êtes-vous sûr de vouloir vous désinscrire de cette activité ?', - header: 'Confirmation', - icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Oui', - rejectLabel: 'Non', - accept: () => this.toggleRegister(activity), + this.isVoluntary$.pipe(take(TAKE_1)).subscribe(isVoluntary => { + if (isVoluntary) { + this.isRegisteredActivity$.pipe(take(TAKE_1)).subscribe(isRegistered => { + if (isRegistered) { + this._confirmationService.confirm({ + message: 'Êtes-vous sûr de vouloir vous désinscrire de cette activité ?', + header: 'Confirmation', + icon: 'pi pi-exclamation-triangle', + acceptLabel: 'Oui', + rejectLabel: 'Non', + dismissableMask: true, + accept: () => this.toggleRegister(activity), + }); + this.toggleRegister(activity); + } else { + this.toggleRegister(activity); + } }); } else { - this.toggleRegister(activity); + this._confirmationService.confirm({ + message: "Seuls les utilisateurs connectés peuvent s'inscrire aux activités.", + header: 'Information', + icon: 'pi pi-info-circle', + acceptLabel: 'Se connecter maintenant', + rejectVisible: false, + dismissableMask: true, + accept: () => this.navigateToLogin(), + }); } }); } @@ -69,4 +92,8 @@ export class ActivityDescriptionComponent implements OnInit { this._activityFacadeService.toggleRegister(activity.id, !isRegistered); }); } + + navigateToLogin(): void { + this._authService.triggerLoginModal(); + } } diff --git a/src/app/features/activity/components/activity-filter-search/activity-filter-search.component.html b/src/app/features/activity/components/activity-filter-search/activity-filter-search.component.html index 9d8f52e1..0c49d7eb 100644 --- a/src/app/features/activity/components/activity-filter-search/activity-filter-search.component.html +++ b/src/app/features/activity/components/activity-filter-search/activity-filter-search.component.html @@ -11,6 +11,7 @@ } @else { (); + userCity$: Observable = this._authFacade.user$.pipe(map(user => (user?.type === UserType.Voluntary ? user.city : null))); + private _searchTerms$ = new Subject(); customFieldConfigs: CustomFieldConfig[] = [ @@ -44,6 +52,21 @@ export class ActivityFilterSearchComponent implements OnInit { ngOnInit(): void { this._initializeSearchTermSubscription(); + + this.userCity$.pipe(this.untilDestroyed()).subscribe(city => { + const locationControl = this.formGroup.get('location'); + if (city) { + locationControl.setValue(city); + this.onInputValuesChanged(); + } + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['resetFormTrigger']) { + this.formGroup.reset(); + this.onInputValuesChanged(); + } } onInputValuesChanged(): void { @@ -52,7 +75,7 @@ export class ActivityFilterSearchComponent implements OnInit { } private _initializeSearchTermSubscription(): void { - this._searchTerms$.pipe(debounceTime(500), distinctUntilChanged()).subscribe(term => { + this._searchTerms$.pipe(debounceTime(500)).subscribe(term => { const filters: ActivitySearchFilters = { search: this.formGroup.value.search ?? '', date: this.formGroup.value.date ?? '', diff --git a/src/app/features/activity/components/activity-filter/activity-filter.component.ts b/src/app/features/activity/components/activity-filter/activity-filter.component.ts index a2e371af..2c9a5418 100644 --- a/src/app/features/activity/components/activity-filter/activity-filter.component.ts +++ b/src/app/features/activity/components/activity-filter/activity-filter.component.ts @@ -31,6 +31,7 @@ import { ButtonStyleClass } from 'src/app/common/models/button'; export class ActivityFilterComponent implements OnInit, AfterViewInit, OnChanges { @Output() selectedThemes: EventEmitter = new EventEmitter(); @Input() formGroup?: FormGroup; + @Input() resetFormTrigger: boolean = false; @Input() formThemeField?: string; @Input() hasLoadedDraft?: boolean; @@ -74,6 +75,11 @@ export class ActivityFilterComponent implements OnInit, AfterViewInit, OnChanges if (changes['hasLoadedDraft'] && this.formGroup) { this.loadDraftThemes(); } + + if (changes['resetFormTrigger']) { + this.selected = []; + this.selectedThemes.emit([...this.selected]); + } } loadDraftThemes(): void { diff --git a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.html b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.html index e3bca851..c2f58846 100644 --- a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.html +++ b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.html @@ -1,6 +1,6 @@ @if (messages$ | async; as messages) { -
+

Ceci est un espace d'échange entre vous et l’association.
@@ -24,7 +24,7 @@

diff --git a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.scss b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.scss index 9dec8ff4..741c7b34 100644 --- a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.scss +++ b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.scss @@ -22,9 +22,9 @@ p-card { } .intro { - background-color: var(--primary-light-color); - color: var(--background-color-white); - padding: 12px 16px; + font-style: italic; + padding: 2px 16px; + border: 1px dashed grey; border-radius: var(--border-radius) var(--border-radius) 0 0; margin-bottom: 8px; text-align: center; diff --git a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.ts b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.ts index a7dae73c..b66c0645 100644 --- a/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.ts +++ b/src/app/features/activity/components/activity-messages-card/activity-messages-card.component.ts @@ -1,5 +1,5 @@ import { AsyncPipe, NgClass } from '@angular/common'; -import { Component, inject, Input, OnInit } from '@angular/core'; +import { Component, ElementRef, inject, Input, OnInit, ViewChild } from '@angular/core'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { Store } from '@ngrx/store'; import { MenuItem } from 'primeng/api'; @@ -24,6 +24,7 @@ export class ActivityMessagesCardComponent implements OnInit { private _activityFacadeService: ActivityFacadeService = inject(ActivityFacadeService); private _fb: FormBuilder = new FormBuilder(); @Input() activityId!: string; + @ViewChild('messageContainer') messageContainer!: ElementRef; messages$!: Observable; emojis: MenuItem[] = [ @@ -69,5 +70,14 @@ export class ActivityMessagesCardComponent implements OnInit { this._activityFacadeService.postActivityMessage(newMessage); this.messageForm.reset(); + setTimeout(() => this._scrollToBottom(), 100); + } + + private _scrollToBottom(): void { + try { + this.messageContainer.nativeElement.scrollTop = this.messageContainer.nativeElement.scrollHeight; + } catch (err) { + console.error('Scroll failed:', err); + } } } diff --git a/src/app/features/activity/components/inscription-badge/inscription-badge.component.scss b/src/app/features/activity/components/inscription-badge/inscription-badge.component.scss index e247a39e..87bd4884 100644 --- a/src/app/features/activity/components/inscription-badge/inscription-badge.component.scss +++ b/src/app/features/activity/components/inscription-badge/inscription-badge.component.scss @@ -11,7 +11,7 @@ text-align: center; width: 75px; height: 30px; - background-color: var(--secondary-validate-color); + background-color: var(--ternary-validate-color); color: var(--background-color-white); font-size: var(--font-size-p-mini); font-family: var(--font-family-p); diff --git a/src/app/features/activity/pages/activities-home/activities-home.component.html b/src/app/features/activity/pages/activities-home/activities-home.component.html index fc2f1ca8..58b7cb68 100644 --- a/src/app/features/activity/pages/activities-home/activities-home.component.html +++ b/src/app/features/activity/pages/activities-home/activities-home.component.html @@ -8,12 +8,14 @@

Des activités bénévoles près de chez vous

@if (isMobile$ | async) { - + + } @else { - + + }
- +
@@ -29,10 +31,9 @@

Des activités bénévoles près de chez vous

} @else { @for (activity of filteredActivities$ | async; track activity.id) { - + } @empty {
-

Aucun résultat ne correspond à votre recherche

diff --git a/src/app/features/activity/pages/activities-home/activities-home.component.scss b/src/app/features/activity/pages/activities-home/activities-home.component.scss index 5639e0ff..3f5f18d2 100644 --- a/src/app/features/activity/pages/activities-home/activities-home.component.scss +++ b/src/app/features/activity/pages/activities-home/activities-home.component.scss @@ -104,6 +104,22 @@ font-size: 18px; font-style: italic; color: var(--dai-secondary, #888); + margin-bottom: 5rem; + + .btn-reset { + margin-top: 1rem; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + background-color: var(--primary-light-color); + color: #fff; + cursor: pointer; + font-weight: 500; + } + + .btn-reset:hover { + background-color: var(--primary-dark-color); + } .btn-reset { margin-top: 1rem; @@ -183,6 +199,7 @@ .cards-and-map { .cards-activities { grid-template-columns: repeat(1, 1fr); + min-height: 20vh; } } } diff --git a/src/app/features/activity/pages/activities-home/activities-home.component.ts b/src/app/features/activity/pages/activities-home/activities-home.component.ts index e1cdab32..6c4aa723 100644 --- a/src/app/features/activity/pages/activities-home/activities-home.component.ts +++ b/src/app/features/activity/pages/activities-home/activities-home.component.ts @@ -55,6 +55,7 @@ export class ActivitiesHomeComponent implements OnInit { ); searchFilters: ActivitySearchFilters = { search: '', date: '', location: '' }; + resetFormTrigger: boolean = false; selectedThemesName: ThemeName[] = []; navigationItems: NavigationItems[] = [{ name: 'Liste' }, { name: 'Carte' }]; chosenNavigation: string = 'Liste'; @@ -121,6 +122,7 @@ export class ActivitiesHomeComponent implements OnInit { } resetFiltersAndShowAll(): void { + this.resetFormTrigger = !this.resetFormTrigger; this.searchFilters = { search: '', date: '', location: '' }; this.selectedThemesName = []; diff --git a/src/app/features/activity/pages/activity-creation/activity-creation.component.html b/src/app/features/activity/pages/activity-creation/activity-creation.component.html index d68765c7..2d30a000 100644 --- a/src/app/features/activity/pages/activity-creation/activity-creation.component.html +++ b/src/app/features/activity/pages/activity-creation/activity-creation.component.html @@ -31,7 +31,8 @@

Informations générales

[useIftaLabel]="true" [fieldConfig]="activityField" [formGroup]="activityForm" - [submitted]="submitted"> + [submitted]="submitted" + dateMode="future"> @@ -86,7 +87,7 @@

Thème(s) de l'activité

type="button" label="Annuler" ariaLabel="annuler la création de l'activité" - (buttonClicked)="navigateToHomePage()" + (buttonClicked)="navigateToPreviousPage()" [styleClass]="ButtonStyleClass.defaultWhite"> (`${this._apiUrl}/themes`); } + getDraftActivities(): Observable { + return this._http.get(`${this._apiUrl}/activities/draft`); + } + getFutureActivities(): Observable { return this._http.get(`${this._apiUrl}/activities/future`); } diff --git a/src/app/features/activity/services/activity-facade.service.ts b/src/app/features/activity/services/activity-facade.service.ts index dd9ed246..ef603a7a 100644 --- a/src/app/features/activity/services/activity-facade.service.ts +++ b/src/app/features/activity/services/activity-facade.service.ts @@ -46,6 +46,13 @@ export class ActivityFacadeService { ); } + getDraftActivitiesFromApi(): void { + this._fetchActivities( + () => this._activitiesApi.getDraftActivities(), + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ); + } + getPastActivitiesFromApi(): void { this._activitiesApi .getPastActivities() @@ -130,12 +137,6 @@ export class ActivityFacadeService { isSaved: isFollowApiResponse, }) ); - - if (isFollowApiResponse) { - showSuccessToast(this._toast); - } else { - showInfoToast(this._toast, 'Activité retirée de vos favoris.'); - } }, }) ) @@ -225,7 +226,6 @@ export class ActivityFacadeService { .pipe( tap((postedMessage: Message) => { this._store.dispatch(MessagesActions.addMessage({ message: postedMessage })); - showSuccessToast(this._toast); }) ) .subscribe(); diff --git a/src/app/features/association/pages/association-details/association-details.component.html b/src/app/features/association/pages/association-details/association-details.component.html index ccd27207..ebe39fb4 100644 --- a/src/app/features/association/pages/association-details/association-details.component.html +++ b/src/app/features/association/pages/association-details/association-details.component.html @@ -10,15 +10,18 @@

Détails de l'association

@if (!navigationItems.length) { -
- @for (activity of activities$ | async; track activity.id) { - - } @empty { -

-
- Aucune activité proposée pour l’instant. -

- } +
+

Découvrez toutes les activités prévues

+
+ @for (activity of activities$ | async; track activity.id) { + + } @empty { +

+
+ Aucune activité proposée pour l’instant. +

+ } +
@@ -28,15 +31,17 @@

Détails de l'association

@if (navigationItems.length && chosenNavigation === 'Activités') { -
- @for (activity of activities$ | async; track activity.id) { - - } @empty { -

-
- Aucune activité proposée pour l’instant. -

- } +
+
+ @for (activity of activities$ | async; track activity.id) { + + } @empty { +

+
+ Aucune activité proposée pour l’instant. +

+ } +
} @if (navigationItems.length && chosenNavigation === 'Association') { diff --git a/src/app/features/association/pages/association-details/association-details.component.scss b/src/app/features/association/pages/association-details/association-details.component.scss index 5a726f1f..b5c67006 100644 --- a/src/app/features/association/pages/association-details/association-details.component.scss +++ b/src/app/features/association/pages/association-details/association-details.component.scss @@ -12,6 +12,7 @@ overflow: hidden; .page-title { + color: var(--primary-light-color); text-align: center; margin-bottom: 1rem; font-size: 1.5rem; @@ -34,46 +35,59 @@ gap: 2vw; min-height: 0; - .cards-activities { + .cards-activities-container { flex: 1; - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - overflow-y: auto; - overflow-x: hidden; - padding-right: 0.5rem; - - scrollbar-width: thin; - scrollbar-color: var(--primary-light-color) transparent; - - &::-webkit-scrollbar { - width: 6px; - } - &::-webkit-scrollbar-thumb { - background-color: var(--primary-light-color); - border-radius: 4px; - } - &::-webkit-scrollbar-track { - background: transparent; - } + display: flex; + flex-direction: column; + min-height: 0; - .empty { - grid-column: 1 / -1; - text-align: center; - font-style: italic; - color: var(--dai-secondary, #888); + .cards-activities-header { + color: var(--primary-light-color); + margin-top: 2rem; + margin-bottom: 3rem; } - @media (max-width: 1500px) { - grid-template-columns: 1fr; - } + .cards-activities { + flex: 1; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + overflow-y: auto; + overflow-x: hidden; + padding-right: 0.5rem; - @media (max-width: 1300px) { - overflow-y: visible; - } + scrollbar-width: thin; + scrollbar-color: var(--primary-light-color) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-thumb { + background-color: var(--primary-light-color); + border-radius: 4px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + + .empty { + grid-column: 1 / -1; + text-align: center; + font-style: italic; + color: var(--dai-secondary, #888); + } + + @media (max-width: 1500px) { + grid-template-columns: 1fr; + } + + @media (max-width: 1300px) { + overflow-y: visible; + } - @media (max-width: 768px) { - grid-template-columns: 1fr; + @media (max-width: 768px) { + grid-template-columns: 1fr; + } } } diff --git a/src/app/features/association/pages/association-details/association-details.component.ts b/src/app/features/association/pages/association-details/association-details.component.ts index f00fba00..426f6f52 100644 --- a/src/app/features/association/pages/association-details/association-details.component.ts +++ b/src/app/features/association/pages/association-details/association-details.component.ts @@ -30,6 +30,7 @@ export class AssociationDetailsComponent implements OnInit { ngOnInit(): void { this._initializeAssociationData(); + this._updateNavigationItems(this.screenWidth); } @HostListener('window:resize', ['$event']) diff --git a/src/app/features/authentication/components/login-modal/login-modal.component.html b/src/app/features/authentication/components/login-modal/login-modal.component.html index 2b5349cd..615d8bb1 100644 --- a/src/app/features/authentication/components/login-modal/login-modal.component.html +++ b/src/app/features/authentication/components/login-modal/login-modal.component.html @@ -9,7 +9,7 @@ [header]="'SE CONNECTER'" styleClass="login-modal">
-
+ @for (field of loginFields; track $index) {
@@ -22,12 +22,14 @@ Se connecter
- -
- + +
+
- +
+ +
diff --git a/src/app/features/authentication/components/login-modal/login-modal.component.scss b/src/app/features/authentication/components/login-modal/login-modal.component.scss index 95fcdd76..3ad0aded 100644 --- a/src/app/features/authentication/components/login-modal/login-modal.component.scss +++ b/src/app/features/authentication/components/login-modal/login-modal.component.scss @@ -66,11 +66,11 @@ } } - .forgot-password-container { + .helper-action-container { display: flex; justify-content: flex-end; - .forgot-password-btn { + .action-btn { background: none; border: none; color: var(--primary-color); @@ -81,6 +81,11 @@ &:focus { outline: 2px solid var(--primary-color); } + + &:hover { + transition: color 0.3s ease; + color: var(--primary-dark-color); + } } } } diff --git a/src/app/features/authentication/components/login-modal/login-modal.component.ts b/src/app/features/authentication/components/login-modal/login-modal.component.ts index befbdc7a..45996513 100644 --- a/src/app/features/authentication/components/login-modal/login-modal.component.ts +++ b/src/app/features/authentication/components/login-modal/login-modal.component.ts @@ -1,11 +1,11 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { Component, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { RadioButtonModule } from 'primeng/radiobutton'; -import { filter, take } from 'rxjs'; +import { combineLatest, filter, take } from 'rxjs'; import { InputFieldComponent } from 'src/app/common/components/input-field/input-field.component'; import { TAKE_1 } from 'src/app/common/constants/observables.constants'; import { InputFieldErrorComponent } from 'src/app/common/components/input-field-error/input-field-error.component'; @@ -42,10 +42,11 @@ import { AuthService } from '../../services/auth.service'; ]), ], }) -export class LoginModalComponent { +export class LoginModalComponent implements OnChanges { @Output() visibleChange = new EventEmitter(); @Output() openPasswordForgottenModal = new EventEmitter(); + @Input() email: string = ''; @Input() visible: boolean = false; private _authFacade = inject(AuthFacade); private _authService = inject(AuthService); @@ -55,7 +56,7 @@ export class LoginModalComponent { openForgotPasswordModal = false; loginForm = this._fb.group({ - email: ['', [Validators.required, Validators.email]], + email: [this.email, [Validators.required, Validators.email]], password: ['', [Validators.required]], }); @@ -67,6 +68,12 @@ export class LoginModalComponent { error: string = ''; submitted = false; + ngOnChanges(changes: SimpleChanges): void { + if (changes['email'] && changes['email'].currentValue !== undefined) { + this.loginForm.get('email')?.setValue(changes['email'].currentValue); + } + } + showModal(): void { this.visible = true; this.error = ''; @@ -96,31 +103,29 @@ export class LoginModalComponent { const credentials = this.loginForm.value as UserLogin; this._authFacade.login(credentials); - let loginSuccess = false; - - this._authFacade.isAuthenticated$ - .pipe( + combineLatest([ + this._authFacade.isAuthenticated$.pipe( filter(isAuth => isAuth), take(TAKE_1) - ) - .subscribe(() => { - loginSuccess = true; + ), + this.isBanned$.pipe(take(TAKE_1)), + ]).subscribe(([isAuth, isBanned]) => { + if (isAuth) { this.hideModal(); - }); - - const timeoutDuration = 100; - setTimeout(() => { - this.isBanned$.pipe(take(1)).subscribe(isBanned => { - if (!loginSuccess && isBanned) { - this.error = 'Trop de tentatives échouées. Veuillez réessayer plus tard.'; - } else { - this.error = 'Informations invalides. Veuillez réessayer.'; - } - }); - }, timeoutDuration); + this.error = null; + } else if (isBanned) { + this.error = 'Trop de tentatives échouées. Veuillez réessayer plus tard.'; + } else { + this.error = 'Informations invalides. Veuillez réessayer.'; + } + }); } onForgotPassword(): void { this.openPasswordForgottenModal.emit(true); } + + onNoAccount(): void { + this._authService.triggerSubscribeModal(); + } } diff --git a/src/app/features/authentication/components/register-modal/register-association-form/register-association-form.component.ts b/src/app/features/authentication/components/register-modal/register-association-form/register-association-form.component.ts index 9cb27cf0..08c7b55b 100644 --- a/src/app/features/authentication/components/register-modal/register-association-form/register-association-form.component.ts +++ b/src/app/features/authentication/components/register-modal/register-association-form/register-association-form.component.ts @@ -29,8 +29,8 @@ export class RegisterAssociationFormComponent { email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.pattern(PASSWORD_REGEX)]], confirmPassword: ['', [Validators.required]], - cguConsent: [false, Validators.requiredTrue], // ✅ obligatoire - rgpdConsent: [false, Validators.requiredTrue], // ✅ obligatoire + cguConsent: [false, Validators.requiredTrue], + rgpdConsent: [false, Validators.requiredTrue], }, { validators: [passwordsMatchValidator], @@ -48,7 +48,7 @@ export class RegisterAssociationFormComponent { authFields: FormField[] = [ { name: 'email', label: 'E-mail', type: 'email', required: true }, { name: 'password', label: 'Mot de passe', type: 'password', required: true, showPasswordRules: true }, - { name: 'confirmPassword', label: 'Confirmation', type: 'password', required: true }, + { name: 'confirmPassword', label: 'Confirmation du mot de passe', type: 'password', required: true }, ]; onSubmit(event?: Event): void { diff --git a/src/app/features/authentication/components/register-modal/register-modal/register-modal.component.ts b/src/app/features/authentication/components/register-modal/register-modal/register-modal.component.ts index 7ea45d03..7e49e413 100644 --- a/src/app/features/authentication/components/register-modal/register-modal/register-modal.component.ts +++ b/src/app/features/authentication/components/register-modal/register-modal/register-modal.component.ts @@ -64,7 +64,7 @@ export class RegisterModalComponent extends DestroyableComponent { .subscribe((success: boolean) => { if (success) { this.hideModal(); - this._authService.triggerLoginModal(); + this._authService.triggerLoginModal(data.email); } }); } diff --git a/src/app/features/authentication/components/register-modal/register-voluntary-form/register-voluntary-form.component.html b/src/app/features/authentication/components/register-modal/register-voluntary-form/register-voluntary-form.component.html index efaf7c2f..1207fbce 100644 --- a/src/app/features/authentication/components/register-modal/register-voluntary-form/register-voluntary-form.component.html +++ b/src/app/features/authentication/components/register-modal/register-voluntary-form/register-voluntary-form.component.html @@ -11,7 +11,7 @@
} @else {
- + { + return this._http.get(`${this._apiUrl}/auth/token`); + } +} diff --git a/src/app/features/authentication/services/auth-facade.service.ts b/src/app/features/authentication/services/auth-facade.service.ts index 23eb517b..13603e19 100644 --- a/src/app/features/authentication/services/auth-facade.service.ts +++ b/src/app/features/authentication/services/auth-facade.service.ts @@ -10,7 +10,6 @@ import { ACTIVITY_LENGTH, UserRole } from '../constants/auth.constants'; import { ApiResponseLogin } from '../models/api-response.model'; import { AssociationLogin, UserHeaderInfo, UserLogin, UserType, VoluntaryLogin } from '../models/user.model'; import { AuthService } from './auth.service'; -import { getInitialUserState } from '../store/meta-reducers'; import { VoluntaryProfileService } from '../../profile/services/voluntary-profil.service'; import { MessageService } from 'primeng/api'; import { showErrorToast, showSuccessToast } from 'src/app/common/utils/toast.utils'; @@ -19,6 +18,7 @@ import { UserSelectors } from '../store/user.selectors'; import { UserActions } from '../store/user.actions'; import { ActivitiesActions } from '../../activity/store/activities.actions'; import { ActivitiesSelectors } from '../../activity/store/activities.selectors'; +import { AuthApiService } from './auth-api.service'; @Injectable({ providedIn: 'root', @@ -27,6 +27,7 @@ export class AuthFacade { private _store = inject(Store); private _authService = inject(AuthService); private _activityFacade = inject(ActivityFacadeService); + private _authApiService = inject(AuthApiService); private _voluntaryProfileService = inject(VoluntaryProfileService); private _associationProfileService = inject(AssociationProfileService); private _toast = inject(MessageService); @@ -70,9 +71,7 @@ export class AuthFacade { return this._authService.changeEmail(password, newEmail).pipe( map(({ token, user }) => { this._authService.saveToken(token); - this._authService.updateAuthState(); this._store.dispatch(UserActions.loginSuccess({ userInfos: user })); - localStorage.setItem('userState', JSON.stringify({ isAuthenticated: true, userInfos: user })); showSuccessToast(this._toast); return { token, user }; @@ -84,6 +83,10 @@ export class AuthFacade { return this._authService.deleteAccount(); } + getConnectedUserId(): Observable { + return this.user$.pipe(map(user => user?.id ?? '')); + } + getUserHeaderInfo(): Observable { return combineLatest([this._authService.getRolesUser(), this.user$]).pipe( map(([roles, user]) => { @@ -106,10 +109,13 @@ export class AuthFacade { ); } - initUserFromStorage(): void { - const userState = getInitialUserState(); - if (userState.isAuthenticated && userState.userInfos) { - this._store.dispatch(UserActions.loginSuccess({ userInfos: userState.userInfos })); + getUserByToken(): void { + const token = this._authService.getToken(); + if (token) { + this._authApiService + .getUserByToken() + .pipe() + .subscribe(response => this._store.dispatch(UserActions.loginSuccess({ userInfos: response.user }))); } } @@ -158,7 +164,6 @@ export class AuthFacade { private _resetSession(): void { this._authService.clearToken(); - this._authService.clearUserState(); } private _handleLoginSuccess({ token, user }: ApiResponseLogin): void { diff --git a/src/app/features/authentication/services/auth.service.ts b/src/app/features/authentication/services/auth.service.ts index 58986ab7..f173a2fb 100644 --- a/src/app/features/authentication/services/auth.service.ts +++ b/src/app/features/authentication/services/auth.service.ts @@ -17,8 +17,10 @@ export class AuthService { private _http: HttpClient = inject(HttpClient); private _apiUrl = environment.apiUrl; private _authState$ = new BehaviorSubject(this._checkTokenValidOnInit()); - private _openLoginModal$ = new Subject(); + private _openLoginModal$ = new Subject(); + private _openSubscribeModal$ = new Subject(); openLoginModal$ = this._openLoginModal$.asObservable(); + openSubscribeModal$ = this._openSubscribeModal$.asObservable(); isbanned$ = new BehaviorSubject(false); registerVoluntary(data: VoluntaryRegister): Observable { @@ -79,10 +81,6 @@ export class AuthService { this.isbanned$.next(value); } - public clearUserState(): void { - localStorage.removeItem('userState'); - } - public updateAuthState(): boolean { const token = this.getToken(); if (!token) { @@ -116,7 +114,6 @@ export class AuthService { public resetUser(): void { this.clearToken(); - this.clearUserState(); this.updateAuthState(); } @@ -160,7 +157,11 @@ export class AuthService { } } - triggerLoginModal(): void { - this._openLoginModal$.next(); + triggerLoginModal(email?: string): void { + this._openLoginModal$.next(email ?? null); + } + + triggerSubscribeModal(): void { + this._openSubscribeModal$.next(); } } diff --git a/src/app/features/authentication/store/meta-reducers.ts b/src/app/features/authentication/store/meta-reducers.ts index 0800d0b7..31378ae6 100644 --- a/src/app/features/authentication/store/meta-reducers.ts +++ b/src/app/features/authentication/store/meta-reducers.ts @@ -1,5 +1,4 @@ import { MetaReducer, ActionReducer } from '@ngrx/store'; -import { UserState } from '../models/user.model'; import { GlobalState } from 'src/app/common/store/global-state.state'; import { UserActions } from './user.actions'; @@ -10,7 +9,6 @@ export function userStorageMetaReducer(reducer: ActionReducer): Act const nextState = reducer(state, action); if (action.type === UserActions.logout.type) { - localStorage.removeItem('userState'); return { ...nextState, user: { @@ -21,30 +19,6 @@ export function userStorageMetaReducer(reducer: ActionReducer): Act }; } - if (nextState.user?.isAuthenticated && nextState.user?.userInfos) { - localStorage.setItem('userState', JSON.stringify(nextState.user)); - } - return nextState; }; } - -export function getInitialUserState(): UserState { - try { - const stored = localStorage.getItem('userState'); - if (stored) { - const parsed = JSON.parse(stored); - if (parsed?.isAuthenticated && parsed?.userInfos) { - return parsed; - } - } - } catch (e) { - console.error('Error parsing userState from localStorage:', e); - } - - return { - userInfos: null, - isAuthenticated: false, - error: null, - }; -} diff --git a/src/app/features/authentication/store/user.reducer.ts b/src/app/features/authentication/store/user.reducer.ts index 4457ef67..ef52e3b9 100644 --- a/src/app/features/authentication/store/user.reducer.ts +++ b/src/app/features/authentication/store/user.reducer.ts @@ -1,9 +1,12 @@ import { createReducer, on } from '@ngrx/store'; import { AssociationLogin, FollowedAssociation, MessageNotification, UserState, UserType, VoluntaryLogin } from '../models/user.model'; -import { getInitialUserState } from './meta-reducers'; import { UserActions } from './user.actions'; -export const initialState: UserState = getInitialUserState(); +export const initialState: UserState = { + userInfos: null, + isAuthenticated: false, + error: null, +}; export const userReducer = createReducer( initialState, diff --git a/src/app/features/authentication/utils/form.validators.ts b/src/app/features/authentication/utils/form.validators.ts index 5377323c..f28dfbf8 100644 --- a/src/app/features/authentication/utils/form.validators.ts +++ b/src/app/features/authentication/utils/form.validators.ts @@ -14,12 +14,25 @@ export function photoRequiredValidator(): ValidatorFn { }; } -export function dateRequiredValidator(): ValidatorFn { - const today = new Date(); +export function futureDateValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + + const inputDate = new Date(control.value); + const today = new Date(); + return inputDate >= today ? null : { futureDate: true }; + }; +} + +export function pastDateValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const date = control.value; - return date && date >= today ? null : { dateRequired: true }; + if (!control.value) return null; + + const inputDate = new Date(control.value); + const today = new Date(); + + return inputDate <= today ? null : { pastDate: true }; }; } diff --git a/src/app/features/profile/components/account-settings-security/account-settings-security.component.ts b/src/app/features/profile/components/account-settings-security/account-settings-security.component.ts index c047f5b3..057bac22 100644 --- a/src/app/features/profile/components/account-settings-security/account-settings-security.component.ts +++ b/src/app/features/profile/components/account-settings-security/account-settings-security.component.ts @@ -55,6 +55,7 @@ export class AccountSettingsSecurityComponent { rejectLabel: 'Annuler', acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-secondary', + dismissableMask: true, accept: () => { this._authFacade.deleteAccount().subscribe({ next: () => { diff --git a/src/app/features/profile/components/activity-list/activity-list.component.html b/src/app/features/profile/components/activity-list/activity-list.component.html index abd224f8..6a52b5c4 100644 --- a/src/app/features/profile/components/activity-list/activity-list.component.html +++ b/src/app/features/profile/components/activity-list/activity-list.component.html @@ -6,7 +6,6 @@ @for (activity of activities; track activity.id) { diff --git a/src/app/features/profile/components/activity-list/activity-list.component.ts b/src/app/features/profile/components/activity-list/activity-list.component.ts index 65df3416..0a63d548 100644 --- a/src/app/features/profile/components/activity-list/activity-list.component.ts +++ b/src/app/features/profile/components/activity-list/activity-list.component.ts @@ -37,6 +37,7 @@ export class ActivityListComponent { message: 'Êtes-vous sûr de vouloir supprimer cette activité ? Cette action est irréversible.', header: 'Confirmation de suppression', icon: 'pi pi-exclamation-triangle', + dismissableMask: true, acceptLabel: 'Oui, supprimer', rejectLabel: 'Annuler', acceptButtonStyleClass: 'p-button-danger', @@ -53,6 +54,6 @@ export class ActivityListComponent { } showEditButton(activityDate: string | Date): boolean { - return new Date(activityDate) > new Date(); + return activityDate ? new Date(activityDate) > new Date() : true; } } diff --git a/src/app/features/profile/components/association/association-activity-menu/association-activity-menu.component.ts b/src/app/features/profile/components/association/association-activity-menu/association-activity-menu.component.ts index e1fd1adc..7d4ddb28 100644 --- a/src/app/features/profile/components/association/association-activity-menu/association-activity-menu.component.ts +++ b/src/app/features/profile/components/association/association-activity-menu/association-activity-menu.component.ts @@ -25,27 +25,11 @@ export class AssociationActivityMenuComponent implements OnInit { activities$: Observable = this._activityFacade.activities$; associationId$: Observable = this._activityFacade.associationId$; chosenNavigation$ = new BehaviorSubject('Futures'); - - get filteredActivities$(): Observable { - return combineLatest([this.activities$, this.associationId$, this.chosenNavigation$]).pipe( - map(([activities, associationId, chosenNavigation]) => { - if (!associationId) return []; - - if (chosenNavigation === 'Futures' || chosenNavigation === 'Passées') { - return activities.filter(activity => activity.association.id === associationId && activity.status === ActivityStatusEnum.PUBLISHED); - } - - if (chosenNavigation === 'Brouillons') { - return activities.filter(activity => activity.association.id === associationId && activity.status === ActivityStatusEnum.DRAFT); - } - - return []; - }) - ); - } + filteredActivities$!: Observable; ngOnInit(): void { this._loadActivitiesForTab(this.chosenNavigation$.value); + this._filterActivitiesForTab(); } handleTabChange(tab: string): void { @@ -59,6 +43,23 @@ export class AssociationActivityMenuComponent implements OnInit { this._activityFacade.getFutureActivitiesFromApi(); } else if (tab === 'Passées') { this._activityFacade.getPastActivitiesFromApi(); + } else if (tab === 'Brouillons') { + this._activityFacade.getDraftActivitiesFromApi(); } } + + private _filterActivitiesForTab(): void { + this.filteredActivities$ = combineLatest([this.activities$, this.associationId$, this.chosenNavigation$]).pipe( + map(([activities, associationId, chosenNavigation]) => { + if (!associationId) return []; + if (['Futures', 'Passées'].includes(chosenNavigation)) { + return activities.filter(activity => activity.association.id === associationId && activity.status === ActivityStatusEnum.PUBLISHED); + } + if (chosenNavigation === 'Brouillons') { + return activities.filter(activity => activity.association.id === associationId && activity.status === ActivityStatusEnum.DRAFT); + } + return []; + }) + ); + } } diff --git a/src/app/features/profile/components/profile-menu/profile-menu.component.scss b/src/app/features/profile/components/profile-menu/profile-menu.component.scss index 30416be7..217a7143 100644 --- a/src/app/features/profile/components/profile-menu/profile-menu.component.scss +++ b/src/app/features/profile/components/profile-menu/profile-menu.component.scss @@ -102,10 +102,11 @@ margin: 0; flex-direction: row; gap: 0; + justify-content: center; //todo: à changer quand dark ok .menu-item { display: flex; - width: 25%; + width: 33%; //todo: à changer quand dark ok align-items: center; flex-direction: column; gap: 4px; diff --git a/src/app/features/profile/components/upload-avatar/upload-avatar.component.ts b/src/app/features/profile/components/upload-avatar/upload-avatar.component.ts index d79e71c2..25311869 100644 --- a/src/app/features/profile/components/upload-avatar/upload-avatar.component.ts +++ b/src/app/features/profile/components/upload-avatar/upload-avatar.component.ts @@ -57,10 +57,10 @@ export class UploadAvatarComponent { } private _isValidFile(file: File): boolean { - const maxFileSize = 2 * 1024 * 1024; + const maxFileSize = 10 * 1024 * 1024; const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp']; if (file.size > maxFileSize) { - showErrorToast(this._toast, 'Le fichier est trop volumineux (max 2MB).'); + showErrorToast(this._toast, 'Le fichier est trop volumineux (max 10MB).'); return false; } if (!allowedTypes.includes(file.type)) { diff --git a/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.html b/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.html index 63a89673..f6d54cc4 100644 --- a/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.html +++ b/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.html @@ -22,7 +22,7 @@

Mes informations personnelles

diff --git a/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.ts b/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.ts index 45b11a19..5dd570a5 100644 --- a/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.ts +++ b/src/app/features/profile/components/voluntary/voluntary-about/voluntary-about.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { MessageService } from 'primeng/api'; @@ -16,11 +16,12 @@ import { VoluntaryUpdateRequestDTO } from '../../../models/update-request-dto.mo import { VoluntaryProfileService } from '../../../services/voluntary-profil.service'; import { EditableFieldComponent } from '../../editable-field/editable-field.component'; import { UploadAvatarComponent } from '../../upload-avatar/upload-avatar.component'; +import { IsoDatePipe } from '../../../../../common/pipes/date-iso-pipe'; @Component({ selector: 'app-voluntary-about', standalone: true, - imports: [AsyncPipe, SingleButtonComponent, ReactiveFormsModule, DatePipe, EditableFieldComponent, UploadAvatarComponent], + imports: [AsyncPipe, SingleButtonComponent, ReactiveFormsModule, EditableFieldComponent, UploadAvatarComponent, IsoDatePipe], templateUrl: './voluntary-about.component.html', styleUrl: './voluntary-about.component.scss', }) diff --git a/src/app/features/profile/components/voluntary/voluntary-profile-menu/voluntary-profile-menu.component.ts b/src/app/features/profile/components/voluntary/voluntary-profile-menu/voluntary-profile-menu.component.ts index 8f3f5f8e..2fbafc1e 100644 --- a/src/app/features/profile/components/voluntary/voluntary-profile-menu/voluntary-profile-menu.component.ts +++ b/src/app/features/profile/components/voluntary/voluntary-profile-menu/voluntary-profile-menu.component.ts @@ -12,6 +12,6 @@ export class VoluntaryProfileMenuComponent { { label: 'Activités', link: '/profile/voluntary/activities', icon: 'fas fa-user-circle' }, { label: 'Profil', link: '/profile/voluntary/about', icon: 'fas fa-id-badge' }, { label: 'Sécurité', link: '/profile/voluntary/security', icon: 'fas fa-lock' }, - { label: 'Préférences', link: '/profile/voluntary/settings', icon: 'fas fa-sliders-h' }, + // { label: 'Préférences', link: '/profile/voluntary/settings', icon: 'fas fa-sliders-h' }, //todo: à changer quand dark ok ]; } diff --git a/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.html b/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.html index e50dd1b2..a0f513db 100644 --- a/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.html +++ b/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.html @@ -10,7 +10,7 @@
@if (reportSelected.status === StatusReportEnum.InProgress) {
- + Sauvegarder diff --git a/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.ts b/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.ts index 77b944ec..d638b8cb 100644 --- a/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.ts +++ b/src/app/features/report/components/report-details-action-admin/report-details-action-admin.component.ts @@ -82,6 +82,7 @@ export class ReportDetailsActionAdminComponent implements OnInit { rejectLabel: 'Annuler', acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-secondary', + dismissableMask: true, accept: () => { const updatedReport: Report = { ...this.reportSelected, @@ -110,6 +111,7 @@ export class ReportDetailsActionAdminComponent implements OnInit { rejectLabel: 'Annuler', acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-secondary', + dismissableMask: true, accept: () => { switch (userBan) { case 'reporter': diff --git a/src/app/features/report/components/report-modal/report-modal.component.html b/src/app/features/report/components/report-modal/report-modal.component.html index b09317bd..c496eaab 100644 --- a/src/app/features/report/components/report-modal/report-modal.component.html +++ b/src/app/features/report/components/report-modal/report-modal.component.html @@ -20,7 +20,7 @@

Commentaire libre

diff --git a/src/styles.scss b/src/styles.scss index 41e81d38..a0cd4066 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -42,6 +42,7 @@ body { --primary-dark-color: #13678a; --primary-light-color: #1385a8; + --ternary-validate-color: #2a9d13; --secondary-validate-color: #36c919; --secondary-complete-color: #fa8d12; --secondary-error-color: #d4412d;