diff --git a/libs/openchallenges/challenge/src/lib/challenge-stats/challenge-stats.component.ts b/libs/openchallenges/challenge/src/lib/challenge-stats/challenge-stats.component.ts index 35ac8beb35..410a8aa94d 100644 --- a/libs/openchallenges/challenge/src/lib/challenge-stats/challenge-stats.component.ts +++ b/libs/openchallenges/challenge/src/lib/challenge-stats/challenge-stats.component.ts @@ -1,14 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { - Challenge, - ChallengeService, -} from '@sagebionetworks/openchallenges/api-client-angular'; -import { catchError, Observable, of, switchMap, throwError } from 'rxjs'; -import { - HttpStatusRedirect, - handleHttpError, -} from '@sagebionetworks/openchallenges/util'; +import { Challenge } from '@sagebionetworks/openchallenges/api-client-angular'; +import { Observable } from 'rxjs'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; @@ -25,32 +17,9 @@ export class ChallengeStatsComponent implements OnInit { mockViews!: number; mockStargazers!: number; - constructor( - private activatedRoute: ActivatedRoute, - private router: Router, - private challengeService: ChallengeService - ) {} - ngOnInit(): void { this.mockViews = 5_000; this.mockStargazers = 2; - - this.challenge$ = this.activatedRoute.params.pipe( - switchMap((params) => - this.challengeService.getChallenge(params['challengeId']) - ), - switchMap((challenge) => { - this.router.navigate(['/challenge', challenge.id, challenge.slug]); - return of(challenge); - }), - catchError((err) => { - const error = handleHttpError(err, this.router, { - 404: '/not-found', - 400: '/challenge', - } as HttpStatusRedirect); - return throwError(() => error); - }) - ); } shorthand(n: number | undefined) { diff --git a/libs/openchallenges/challenge/src/lib/challenge.component.html b/libs/openchallenges/challenge/src/lib/challenge.component.html index 043fd77304..d836b50050 100644 --- a/libs/openchallenges/challenge/src/lib/challenge.component.html +++ b/libs/openchallenges/challenge/src/lib/challenge.component.html @@ -28,24 +28,21 @@

Overview Contributors Organizers diff --git a/libs/openchallenges/challenge/src/lib/challenge.component.ts b/libs/openchallenges/challenge/src/lib/challenge.component.ts index 94fddbd6e7..8b25e33a02 100644 --- a/libs/openchallenges/challenge/src/lib/challenge.component.ts +++ b/libs/openchallenges/challenge/src/lib/challenge.component.ts @@ -1,21 +1,18 @@ -import { Component, OnInit, Renderer2 } from '@angular/core'; -import { - ActivatedRoute, - ParamMap, - Router, - RouterModule, -} from '@angular/router'; +import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Challenge, ChallengeService, } from '@sagebionetworks/openchallenges/api-client-angular'; import { catchError, + combineLatest, map, Observable, - of, + shareReplay, Subscription, switchMap, + take, throwError, } from 'rxjs'; import { Tab } from './tab.model'; @@ -37,9 +34,10 @@ import { ChallengeOrganizersComponent } from './challenge-organizers/challenge-o import { ChallengeOverviewComponent } from './challenge-overview/challenge-overview.component'; import { ChallengeStargazersComponent } from './challenge-stargazers/challenge-stargazers.component'; import { ChallengeStatsComponent } from './challenge-stats/challenge-stats.component'; -import { CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { SeoService } from '@sagebionetworks/shared/util'; import { getSeoData } from './challenge-seo-data'; +import { HttpParams } from '@angular/common/http'; @Component({ selector: 'openchallenges-challenge', @@ -60,7 +58,7 @@ import { getSeoData } from './challenge-seo-data'; templateUrl: './challenge.component.html', styleUrls: ['./challenge.component.scss'], }) -export class ChallengeComponent implements OnInit { +export class ChallengeComponent implements OnInit, OnDestroy { public appVersion: string; public dataUpdatedOn: string; public privacyPolicyUrl: string; @@ -69,13 +67,10 @@ export class ChallengeComponent implements OnInit { challenge$!: Observable; loggedIn = false; - // progressValue = 0; - // remainDays!: number | undefined; challengeAvatar!: Avatar; tabs = CHALLENGE_TABS; - tabKeys: string[] = Object.keys(this.tabs); activeTab!: Tab; - private subscriptions: Subscription[] = []; + private subscription = new Subscription(); constructor( private activatedRoute: ActivatedRoute, @@ -83,7 +78,8 @@ export class ChallengeComponent implements OnInit { private challengeService: ChallengeService, private readonly configService: ConfigService, private seoService: SeoService, - private renderer2: Renderer2 + private renderer2: Renderer2, + private _location: Location ) { this.appVersion = this.configService.config.appVersion; this.dataUpdatedOn = this.configService.config.dataUpdatedOn; @@ -97,61 +93,62 @@ export class ChallengeComponent implements OnInit { switchMap((params) => this.challengeService.getChallenge(params['challengeId']) ), - switchMap((challenge) => { - this.router.navigate(['/challenge', challenge.id, challenge.slug]); - return of(challenge); - }), catchError((err) => { const error = handleHttpError(err, this.router, { 404: '/not-found', 400: '/challenge', } as HttpStatusRedirect); return throwError(() => error); - }) + }), + shareReplay(1), + take(1) ); this.challenge$.subscribe((challenge) => { this.challengeAvatar = { name: challenge.name, - src: challenge.avatarUrl || '', + src: challenge.avatarUrl ?? '', size: 250, }; this.seoService.setData(getSeoData(challenge), this.renderer2); + }); - // this.progressValue = - // challenge.startDate && challenge.endDate - // ? this.calcProgress( - // new Date().toUTCString(), - // challenge.startDate, - // challenge.endDate - // ) - // : 0; + const activeTabKey$: Observable = + this.activatedRoute.queryParams.pipe( + map((params) => + Object.keys(this.tabs).includes(params['tab']) + ? params['tab'] + : 'overview' + ) + ); - // this.remainDays = challenge.endDate - // ? this.calcDays(new Date().toUTCString(), challenge.endDate) - // : undefined; + const combineSub = combineLatest({ + challenge: this.challenge$, + activeTabKey: activeTabKey$, + }).subscribe(({ challenge, activeTabKey }) => { + // add slug in url and active param if any + const newPath = `/challenge/${challenge.id}/${challenge.slug}`; + this.updateTab(activeTabKey, newPath); }); - const activeTabSub = this.activatedRoute.queryParamMap - .pipe( - map((params: ParamMap) => params.get('tab')), - map((key) => (key === null ? 'overview' : key)) - ) - .subscribe((key) => (this.activeTab = this.tabs[key])); + this.subscription.add(combineSub); + } - this.subscriptions.push(activeTabSub); + ngOnDestroy(): void { + this.subscription.unsubscribe(); } - // calcDays(startDate: string, endDate: string): number { - // const timeDiff = +new Date(endDate) - +new Date(startDate); - // return Math.round(timeDiff / (1000 * 60 * 60 * 24)); - // } + updateTab(activeTabKey: string, path?: string) { + // update tab param in the url + const queryParams = { tab: activeTabKey }; + const newParam = new HttpParams({ + fromObject: queryParams, + }); + const newPath = path ?? location.pathname; + this._location.replaceState(newPath, newParam.toString()); - // calcProgress(today: string, startDate: string, endDate: string): number { - // return ( - // (this.calcDays(startDate, today) / this.calcDays(startDate, endDate)) * - // 100 - // ); - // } + // update active tab + this.activeTab = this.tabs[activeTabKey]; + } } diff --git a/libs/openchallenges/org-profile/src/lib/org-profile.component.html b/libs/openchallenges/org-profile/src/lib/org-profile.component.html index c75bdc75af..9fb3183551 100644 --- a/libs/openchallenges/org-profile/src/lib/org-profile.component.html +++ b/libs/openchallenges/org-profile/src/lib/org-profile.component.html @@ -18,7 +18,7 @@

- +
@@ -26,24 +26,21 @@

diff --git a/libs/openchallenges/org-profile/src/lib/org-profile.component.ts b/libs/openchallenges/org-profile/src/lib/org-profile.component.ts index d398a44495..94eb607a8f 100644 --- a/libs/openchallenges/org-profile/src/lib/org-profile.component.ts +++ b/libs/openchallenges/org-profile/src/lib/org-profile.component.ts @@ -1,12 +1,8 @@ -import { Component, OnInit, Renderer2 } from '@angular/core'; -import { - ActivatedRoute, - ParamMap, - Router, - RouterModule, -} from '@angular/router'; +import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { catchError, + combineLatest, forkJoin, map, Observable, @@ -38,7 +34,7 @@ import { HttpStatusRedirect, handleHttpError, } from '@sagebionetworks/openchallenges/util'; -import { CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; import { OrgProfileChallengesComponent } from './org-profile-challenges/org-profile-challenges.component'; import { OrgProfileMembersComponent } from './org-profile-members/org-profile-members.component'; @@ -46,6 +42,7 @@ import { OrgProfileOverviewComponent } from './org-profile-overview/org-profile- import { OrgProfileStatsComponent } from './org-profile-stats/org-profile-stats.component'; import { SeoService } from '@sagebionetworks/shared/util'; import { getSeoData } from './org-profile-seo-data'; +import { HttpParams } from '@angular/common/http'; @Component({ selector: 'openchallenges-org-profile', @@ -64,7 +61,7 @@ import { getSeoData } from './org-profile-seo-data'; templateUrl: './org-profile.component.html', styleUrls: ['./org-profile.component.scss'], }) -export class OrgProfileComponent implements OnInit { +export class OrgProfileComponent implements OnInit, OnDestroy { public appVersion: string; public dataUpdatedOn: string; public privacyPolicyUrl: string; @@ -74,11 +71,9 @@ export class OrgProfileComponent implements OnInit { organization$!: Observable; organizationAvatar$!: Observable; loggedIn = true; - // organizationAvatar!: Avatar; tabs = ORG_PROFILE_TABS; - tabKeys: string[] = Object.keys(this.tabs); activeTab!: Tab; - private subscriptions: Subscription[] = []; + private subscription = new Subscription(); constructor( private activatedRoute: ActivatedRoute, @@ -87,7 +82,8 @@ export class OrgProfileComponent implements OnInit { private organizationService: OrganizationService, private imageService: ImageService, private seoService: SeoService, - private renderer2: Renderer2 + private renderer2: Renderer2, + private _location: Location ) { this.appVersion = this.configService.config.appVersion; this.dataUpdatedOn = this.configService.config.dataUpdatedOn; @@ -134,21 +130,29 @@ export class OrgProfileComponent implements OnInit { switchMap((org) => this.getOrganizationImageUrl(org, ImageHeight._500px)) ); - const activeTabSub = this.activatedRoute.queryParamMap - .pipe( - map((params: ParamMap) => params.get('tab')), - map((key) => (key === null ? 'overview' : key)) - ) - .subscribe((key) => (this.activeTab = this.tabs[key])); - - this.subscriptions.push(activeTabSub); + const activeTabKey$: Observable = + this.activatedRoute.queryParams.pipe( + map((params) => + Object.keys(this.tabs).includes(params['tab']) + ? params['tab'] + : 'overview' + ) + ); - forkJoin({ + const combineSub = combineLatest({ org: this.organization$, image: seoOrgImage$, - }).subscribe(({ org, image }) => { + activeTabKey: activeTabKey$, + }).subscribe(({ org, image, activeTabKey }) => { this.seoService.setData(getSeoData(org, image.url), this.renderer2); + this.updateTab(activeTabKey); // add active param if any }); + + this.subscription.add(combineSub); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); } private getOrganizationImageUrl( @@ -170,4 +174,17 @@ export class OrgProfileComponent implements OnInit { }) ); } + + updateTab(activeTabKey: string, path?: string) { + // update tab param in the url + const queryParams = { tab: activeTabKey }; + const newParam = new HttpParams({ + fromObject: queryParams, + }); + const newPath = path ?? location.pathname; + this._location.replaceState(newPath, newParam.toString()); + + // update active tab + this.activeTab = this.tabs[activeTabKey]; + } }