Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(openchallenges): refactor tab switching to preserve scroll position on profile pages #2316

Merged
merged 14 commits into from
Nov 4, 2023
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,21 @@ <h2>
<div class="profile-nav-group">
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'overview' }"
(click)="updateTab('overview')"
[ngClass]="{ 'active-tab': activeTab === tabs['overview'] }"
>
Overview
</a>
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'contributors' }"
(click)="updateTab('contributors')"
[ngClass]="{ 'active-tab': activeTab === tabs['contributors'] }"
>
Contributors
</a>
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'organizers' }"
(click)="updateTab('organizers')"
[ngClass]="{ 'active-tab': activeTab === tabs['organizers'] }"
>
Organizers
Expand Down
95 changes: 46 additions & 49 deletions libs/openchallenges/challenge/src/lib/challenge.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -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;
Expand All @@ -69,21 +67,19 @@ export class ChallengeComponent implements OnInit {

challenge$!: Observable<Challenge>;
loggedIn = false;
// progressValue = 0;
// remainDays!: number | undefined;
challengeAvatar!: Avatar;
tabs = CHALLENGE_TABS;
tabKeys: string[] = Object.keys(this.tabs);
activeTab!: Tab;
private subscriptions: Subscription[] = [];
private subscriptions = new Subscription();

constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
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;
Expand All @@ -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<string> =
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.subscriptions.add(combineSub);
}

this.subscriptions.push(activeTabSub);
ngOnDestroy(): void {
this.subscriptions.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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,29 @@ <h2>
<section id="profile-stats" class="row">
<div class="col fill-empty"></div>
<div class="col">
<openchallenges-org-profile-stats [loggedIn]="loggedIn"/>
<openchallenges-org-profile-stats [loggedIn]="loggedIn" />
</div>
</section>
<section id="profile-bottom" class="content">
<div class="profile-sidenav col">
<div class="profile-nav-group">
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'overview' }"
(click)="updateTab('overview')"
[ngClass]="{ 'active-tab': activeTab === tabs['overview'] }"
>
Overview
</a>
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'challenges' }"
(click)="updateTab('challenges')"
[ngClass]="{ 'active-tab': activeTab === tabs['challenges'] }"
>
Challenges
</a>
<a
class="profile-nav-item"
routerLink="."
[queryParams]="{ tab: 'members' }"
(click)="updateTab('members')"
[ngClass]="{ 'active-tab': activeTab === tabs['members'] }"
>
Members
Expand All @@ -60,7 +57,10 @@ <h2>
*ngSwitchCase="tabs['challenges']"
[organization]="org"
/>
<openchallenges-org-profile-members *ngSwitchCase="tabs['members']" [organization]="org"/>
<openchallenges-org-profile-members
*ngSwitchCase="tabs['members']"
[organization]="org"
/>
</ng-container>
</div>
</section>
Expand Down
Loading