diff --git a/package-lock.json b/package-lock.json index 58cd8c89..8da824b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "primeng": "^19.0.8", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "uuid": "^11.1.0", "zone.js": "~0.15.0" }, "devDependencies": { @@ -2798,6 +2799,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@compodoc/compodoc/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@compodoc/live-server": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz", @@ -19377,16 +19392,16 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 8b15b8c6..a89a798d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "primeng": "^19.0.8", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "uuid": "^11.1.0", "zone.js": "~0.15.0" }, "devDependencies": { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index f8e7d787..9a51c428 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -8,6 +8,7 @@ import { provideHttpClient } from '@angular/common/http'; import { providePrimeNG } from 'primeng/config'; import { activitiesReducer } from './features/activity/store/activities.reducers'; import { myPreset } from './mytheme'; +import { associationsReducer } from './features/association/store/association.reducers'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,7 +16,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideHttpClient(), provideAnimationsAsync(), - provideStore({ activities: activitiesReducer }), + provideStore({ activities: activitiesReducer, associations: associationsReducer }), provideStoreDevtools({ maxAge: 25 }), providePrimeNG({ theme: { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 081b8c0f..e90e2389 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,6 +1,7 @@ import { Routes } from '@angular/router'; import { ActivitiesHomeComponent } from './features/activity/pages/activities-home/activities-home.component'; -import { DemoComponent } from './features/activity/pages/demo/demo.component'; +import { ActivityDetailsComponent } from './features/activity/pages/activity-details/activity-details.component'; + export const routes: Routes = [ { @@ -8,7 +9,7 @@ export const routes: Routes = [ component: ActivitiesHomeComponent, }, { - path: 'demo', - component: DemoComponent, + path: 'activity/:id', + component: ActivityDetailsComponent, }, ]; diff --git a/src/app/features/activity/components/activity-card/activity-card.component.html b/src/app/features/activity/components/activity-card/activity-card.component.html index 84ad3d54..237a7c1d 100644 --- a/src/app/features/activity/components/activity-card/activity-card.component.html +++ b/src/app/features/activity/components/activity-card/activity-card.component.html @@ -1,4 +1,4 @@ -
+
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 fcaa54d4..7ec463d4 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 @@ -1,14 +1,15 @@ import { Component, Input } from '@angular/core'; import { Activity } from '../../models/activity.model'; +import { RouterLink } from '@angular/router'; import { InscriptionBadgeComponent } from '../inscription-badge/inscription-badge.component'; import { FavoriteHeartComponent } from '../favorite-heart/favorite-heart.component'; import { environment } from 'src/environments/environment.development'; -import { TruncatePipe } from '../../../../common/Pipes/TruncateString.pipe'; +import { TruncatePipe } from '../../../../common/pipes/TruncateString.pipe'; import { DatePipe } from '@angular/common'; @Component({ selector: 'app-activity-card', - imports: [InscriptionBadgeComponent, FavoriteHeartComponent, TruncatePipe, DatePipe], + imports: [InscriptionBadgeComponent, FavoriteHeartComponent, TruncatePipe, DatePipe, RouterLink], templateUrl: './activity-card.component.html', styleUrl: './activity-card.component.scss', standalone: true, diff --git a/src/app/features/activity/models/activity.model.ts b/src/app/features/activity/models/activity.model.ts index 697acce7..44f24364 100644 --- a/src/app/features/activity/models/activity.model.ts +++ b/src/app/features/activity/models/activity.model.ts @@ -6,7 +6,8 @@ export type Localisation = { latitude: number; }; -export type Association = { +export type AssociationActiviy = { + id: string; name: string; isFollow: boolean; logo: string; @@ -23,7 +24,8 @@ export class Activity { title: string = ''; description: string = ''; image: string[] = []; - association: Association = { + association: AssociationActiviy = { + id: '', name: '', logo: '', isFollow: false, @@ -49,6 +51,7 @@ export class Activity { this.description = data.description || ''; this.image = data.image || []; this.association = data.association || { + id: '', name: '', logo: '', isFollow: false, diff --git a/src/app/features/activity/pages/activity-details/activity-details.component.html b/src/app/features/activity/pages/activity-details/activity-details.component.html new file mode 100644 index 00000000..3efa6394 --- /dev/null +++ b/src/app/features/activity/pages/activity-details/activity-details.component.html @@ -0,0 +1,6 @@ +
+
+
+ +
+
diff --git a/src/app/features/activity/pages/activity-details/activity-details.component.scss b/src/app/features/activity/pages/activity-details/activity-details.component.scss new file mode 100644 index 00000000..1c54f54e --- /dev/null +++ b/src/app/features/activity/pages/activity-details/activity-details.component.scss @@ -0,0 +1,14 @@ +.activity-details-page { + display: flex; + height: 80vh; + + .activity { + width: 50%; + border: 1px solid black; + } + + .association { + width: 50%; + border: 1px solid black; + } +} diff --git a/src/app/features/activity/pages/activity-details/activity-details.component.ts b/src/app/features/activity/pages/activity-details/activity-details.component.ts new file mode 100644 index 00000000..47c4637b --- /dev/null +++ b/src/app/features/activity/pages/activity-details/activity-details.component.ts @@ -0,0 +1,20 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { AssociationCardComponent } from '../../../association/components/association-card/association-card.component'; +import { ActivatedRoute, ParamMap } from '@angular/router'; + +@Component({ + selector: 'app-activity-details', + imports: [AssociationCardComponent], + templateUrl: './activity-details.component.html', + styleUrl: './activity-details.component.scss', +}) +export class ActivityDetailsComponent implements OnInit { + route: ActivatedRoute = inject(ActivatedRoute); + activityId!: string; + + ngOnInit(): void { + this.route.paramMap.subscribe((params: ParamMap) => { + this.activityId = String(params.get('id')); + }); + } +} diff --git a/src/app/features/activity/store/activities.selector.ts b/src/app/features/activity/store/activities.selector.ts index 0c6d0dae..51b16503 100644 --- a/src/app/features/activity/store/activities.selector.ts +++ b/src/app/features/activity/store/activities.selector.ts @@ -1,4 +1,10 @@ -import { createFeatureSelector } from '@ngrx/store'; +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; import { Activity } from '../models/activity.model'; +import { UUIDTypes } from 'uuid'; -export const selectActivities = createFeatureSelector('activities'); +export const selectActivitiesState = createFeatureSelector('activities'); + +export const selectActivities = createSelector(selectActivitiesState, activities => activities); + +export const selectActivityById = (activityId: UUIDTypes): MemoizedSelector => + createSelector(selectActivitiesState, activities => (activities || []).find(item => item.id === activityId) || null); diff --git a/src/app/features/association/components/association-card/association-card.component.html b/src/app/features/association/components/association-card/association-card.component.html new file mode 100644 index 00000000..2cc208b8 --- /dev/null +++ b/src/app/features/association/components/association-card/association-card.component.html @@ -0,0 +1,7 @@ +

association-card works!

+ +
+ @if (association$ | async; as association) { +

{{ association.description }}

+ } +
diff --git a/src/app/features/association/components/association-card/association-card.component.scss b/src/app/features/association/components/association-card/association-card.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/features/association/components/association-card/association-card.component.ts b/src/app/features/association/components/association-card/association-card.component.ts new file mode 100644 index 00000000..90d295d3 --- /dev/null +++ b/src/app/features/association/components/association-card/association-card.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, inject, Input, OnInit } from '@angular/core'; +import { AssociationFacadeService } from '../../services/association-facade.service'; +import { Observable } from 'rxjs'; +import { Association } from '../../model/association.model'; +import { AsyncPipe } from '@angular/common'; + +@Component({ + selector: 'app-association-card', + imports: [AsyncPipe], + templateUrl: './association-card.component.html', + styleUrl: './association-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AssociationCardComponent implements OnInit { + @Input() activityId!: string; + + associationFacadeService: AssociationFacadeService = inject(AssociationFacadeService); + + association$!: Observable; + + ngOnInit(): void { + this.association$ = this.associationFacadeService.getAssociationCard(this.activityId); + } +} diff --git a/src/app/features/association/model/association.model.ts b/src/app/features/association/model/association.model.ts new file mode 100644 index 00000000..018f3e05 --- /dev/null +++ b/src/app/features/association/model/association.model.ts @@ -0,0 +1,32 @@ +import { UUIDTypes } from 'uuid'; + +export type Statistic = { + value: number; + description: string; +}; + +export class Association { + id: UUIDTypes; + description: string; + founder: string; + LocalDate: Date; + name: string; + associationProfileImageURL: string; + associationLogoImage: string; + siteURL: string; + statistics: Statistic[]; + isFollow: boolean; + + constructor(data: Partial) { + this.id = data.id || ''; + this.description = data.description || ''; + this.founder = data.founder || ''; + this.LocalDate = data.LocalDate || new Date(); + this.name = data.name || ''; + this.associationProfileImageURL = data.associationProfileImageURL || ''; + this.associationLogoImage = data.associationLogoImage || ''; + this.siteURL = data.siteURL || ''; + this.statistics = data.statistics || [{ value: 0, description: '' }]; + this.isFollow = data.isFollow || false; + } +} diff --git a/src/app/features/association/services/association-api.service.ts b/src/app/features/association/services/association-api.service.ts new file mode 100644 index 00000000..287763cb --- /dev/null +++ b/src/app/features/association/services/association-api.service.ts @@ -0,0 +1,19 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { UUIDTypes } from 'uuid'; +import { Association } from '../model/association.model'; +import { environment } from 'src/environments/environment.development'; + +@Injectable({ + providedIn: 'root', +}) +export class AssociationApiService { + _http: HttpClient = inject(HttpClient); + + private _apiUrl = environment.apiUrl; + + getAssociationCard(id: UUIDTypes): Observable { + return this._http.get(`${this._apiUrl}/association/${id}`); + } +} diff --git a/src/app/features/association/services/association-facade.service.ts b/src/app/features/association/services/association-facade.service.ts new file mode 100644 index 00000000..45ef4ebe --- /dev/null +++ b/src/app/features/association/services/association-facade.service.ts @@ -0,0 +1,92 @@ +import { inject, Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AssociationApiService } from './association-api.service'; +import { filter, Observable, of, switchMap, take, tap } from 'rxjs'; +import { setAssociations } from '../store/association.actions'; +import { Association } from '../model/association.model'; +import { selectAssociation } from '../store/association.selector'; +import { selectActivityById } from '../../activity/store/activities.selector'; +import { ActivityFacadeService } from '../../activity/services/activity-facade.service'; + +@Injectable({ + providedIn: 'root', +}) +export class AssociationFacadeService { + store: Store = inject(Store); + activityFacadeService: ActivityFacadeService = inject(ActivityFacadeService); + associationApiService: AssociationApiService = inject(AssociationApiService); + + getAssociationCard(activityId: string): Observable { + // get activity selected from store + return this.store.select(selectActivityById(activityId)).pipe( + switchMap(activity => { + if (activity) { + // get association selected from store + return this._getAssociation(activity.association.id); + } + + // if no activity found try to fetch it from API (i.e : page has been refreshed) + this.activityFacadeService.getAllActivities(); + + // then get activity before getting association + return this.store.select(selectActivityById(activityId)).pipe( + filter(activity => !!activity), // to prevent initial empty store on init to stop the flow + take(1), // stop to listen once store is filled with an activity + switchMap(activity => this._getAssociation(activity!.association.id)) + ); + }) + ); + } + + private _getAssociation(associationId: string): Observable { + return this.store.select(selectAssociation(associationId)).pipe( + switchMap(association => { + if (association) { + return of(association); + } + // If not found in store, fetch from API + return this.associationApiService.getAssociationCard(associationId).pipe( + tap((fetchedAssociation: Association) => { + this.store.dispatch(setAssociations({ association: fetchedAssociation })); + }) + ); + }) + ); + } + // getAssociationCard(activityId: string): Observable { + // // get activity selected from store + // const activity$: Observable = this.store.select(selectActivityById(activityId)); + // // if no activity found try to fetch it from API (i.e : page has been refreshed) + + // return activity$.pipe( + // switchMap(activity => { + // if (!activity) { + // console.log('coucou maman 2 '); + // this.activityFacadeService.getAllActivities(); + // activity = this.store.select(selectActivityById(activityId)); + // } + // if (!activity) { + // return of(null); + // } + // // get associationId from activity + // const associationId = activity.association.id; + + // // get association selected from store + // return this.store.select(selectAssociation(associationId)).pipe( + // switchMap(association => { + // if (association) { + // return of(association); + // } + // // if not found in store, request it from API + // return this.associationApiService.getAssociationCard(associationId).pipe( + // // update store with API response + // tap((fetchedAssociation: Association) => { + // this.store.dispatch(setAssociations({ association: fetchedAssociation })); + // }) + // ); + // }) + // ); + // }) + // ); + // } +} diff --git a/src/app/features/association/store/association.actions.ts b/src/app/features/association/store/association.actions.ts new file mode 100644 index 00000000..51c0995b --- /dev/null +++ b/src/app/features/association/store/association.actions.ts @@ -0,0 +1,6 @@ +import { createAction, props } from '@ngrx/store'; +import { Association } from '../model/association.model'; + +export const setAssociations = createAction('[associations] setAssociations', props<{ association: Association }>()); + +export const selectAssociation = createAction('[associations] getAssociation', props<{ associationId: string }>()); diff --git a/src/app/features/association/store/association.reducers.ts b/src/app/features/association/store/association.reducers.ts new file mode 100644 index 00000000..6f49a1bb --- /dev/null +++ b/src/app/features/association/store/association.reducers.ts @@ -0,0 +1,16 @@ +import { createReducer, on } from '@ngrx/store'; +import { Association } from '../model/association.model'; +import { setAssociations } from './association.actions'; + +export const initialAssociationsState: Association[] = []; + +export const associationsReducer = createReducer( + initialAssociationsState, + on(setAssociations, (state, { association }) => { + const exists = state.some(item => item.id === association.id); + if (exists) { + return state.map(item => (item.id === association.id ? { ...item, ...association } : item)); // Merge if association exist + } + return [...state, association]; + }) +); diff --git a/src/app/features/association/store/association.selector.ts b/src/app/features/association/store/association.selector.ts new file mode 100644 index 00000000..cf755cfb --- /dev/null +++ b/src/app/features/association/store/association.selector.ts @@ -0,0 +1,10 @@ +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { Association } from '../model/association.model'; +import { UUIDTypes } from 'uuid'; + +export const selectAssociationsState = createFeatureSelector('associations'); + +export const selectAssociations = createSelector(selectAssociationsState, associations => associations); + +export const selectAssociation = (associationId: UUIDTypes): MemoizedSelector => + createSelector(selectAssociationsState, associations => (associations || []).find(item => item.id === associationId) || null); diff --git a/src/app/features/association/store/association.state.ts b/src/app/features/association/store/association.state.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/features/header/header.component.spec.ts b/src/app/features/header/header.component.spec.ts deleted file mode 100644 index 1cb98da0..00000000 --- a/src/app/features/header/header.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HeaderComponent } from './header.component'; - -describe('HeaderComponent', () => { - let component: HeaderComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [HeaderComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(HeaderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -});