Skip to content

Commit a83a706

Browse files
authored
Merge pull request #3074 from codecrafters-io/fetch-on-demand-user-entries
Fetch surrounding entries on-demand
2 parents a75c45b + 2cf8a76 commit a83a706

File tree

13 files changed

+177
-168
lines changed

13 files changed

+177
-168
lines changed

app/components/leaderboard-page/entries-table.hbs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
<LeaderboardPage::EntriesTable::Row @entry={{entry}} @rankText={{concat "#" (add index 1)}} />
2727
{{/each}}
2828

29-
{{#if this.shouldShowSurroundingEntries}}
30-
<LeaderboardPage::EntriesTable::FillerRow />
29+
{{#if this.loadUserSpecificResourcesTask.isRunning}}
30+
<LeaderboardPage::EntriesTable::FillerRow @text="... loading ..." />
31+
{{else if this.shouldShowSurroundingEntries}}
32+
<LeaderboardPage::EntriesTable::FillerRow @text="... other users ..." />
3133

32-
{{#each this.sortedSurroundingEntries as |entry|}}
33-
<LeaderboardPage::EntriesTable::Row @entry={{entry}} @rankText={{"#~5000"}} />
34+
{{#each this.sortedSurroundingEntriesWithRanks as |entryWithRank|}}
35+
<LeaderboardPage::EntriesTable::Row @entry={{entryWithRank.entry}} @rankText={{concat "#" entryWithRank.rank}} />
3436
{{/each}}
3537
{{/if}}
3638
</tbody>

app/components/leaderboard-page/entries-table.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import Component from '@glimmer/component';
2-
import { inject as service } from '@ember/service';
3-
import type Store from '@ember-data/store';
4-
import type LeaderboardEntryModel from 'codecrafters-frontend/models/leaderboard-entry';
52
import type AuthenticatorService from 'codecrafters-frontend/services/authenticator';
63
import type LanguageModel from 'codecrafters-frontend/models/language';
4+
import type LeaderboardEntryModel from 'codecrafters-frontend/models/leaderboard-entry';
5+
import type LeaderboardRankCalculationModel from 'codecrafters-frontend/models/leaderboard-rank-calculation';
6+
import type Store from '@ember-data/store';
7+
import { inject as service } from '@ember/service';
8+
import { task } from 'ember-concurrency';
9+
import { tracked } from '@glimmer/tracking';
710

811
interface Signature {
912
Element: HTMLDivElement;
1013

1114
Args: {
1215
language: LanguageModel;
13-
surroundingEntries: LeaderboardEntryModel[];
1416
topEntries: LeaderboardEntryModel[];
1517
};
1618
}
@@ -19,6 +21,17 @@ export default class LeaderboardPageEntriesTable extends Component<Signature> {
1921
@service declare authenticator: AuthenticatorService;
2022
@service declare store: Store;
2123

24+
@tracked surroundingEntries: LeaderboardEntryModel[] = [];
25+
@tracked userRankCalculation: LeaderboardRankCalculationModel | null = null;
26+
27+
constructor(owner: unknown, args: Signature['Args']) {
28+
super(owner, args);
29+
30+
if (this.authenticator.isAuthenticated) {
31+
this.loadUserSpecificResourcesTask.perform();
32+
}
33+
}
34+
2235
get explanationMarkdownForScore() {
2336
return `
2437
The highest possible score for this track is ${this.args.language.leaderboard!.highestPossibleScore}.
@@ -36,24 +49,64 @@ Harder stages have higher scores assigned to them.
3649
}
3750

3851
get shouldShowSurroundingEntries(): boolean {
39-
return !!(this.authenticator.isAuthenticated && !this.userIsInTopLeaderboardEntries && this.sortedSurroundingEntries.length > 0);
52+
return !this.userIsInTopLeaderboardEntries && this.surroundingEntries.length > 0;
4053
}
4154

4255
get sortedSurroundingEntries() {
43-
return this.args.surroundingEntries.filter((entry) => !entry.isBanned).sort((a, b) => b.score - a.score);
56+
return this.surroundingEntries.filter((entry) => !entry.isBanned).sort((a, b) => b.score - a.score);
57+
}
58+
59+
get sortedSurroundingEntriesWithRanks() {
60+
return this.sortedSurroundingEntries.map((entry, index) => ({
61+
entry: entry,
62+
rank: this.userRankCalculation!.rank + (index - this.userEntryIndexInSurroundingEntries),
63+
}));
4464
}
4565

4666
get sortedTopEntries() {
4767
return this.args.topEntries.filter((entry) => !entry.isBanned).sort((a, b) => b.score - a.score);
4868
}
4969

70+
get userEntryIndexInSurroundingEntries() {
71+
return this.sortedSurroundingEntries.findIndex((entry) => entry.user.id === this.authenticator.currentUserId);
72+
}
73+
5074
get userIsInTopLeaderboardEntries(): boolean {
5175
if (!this.authenticator.isAuthenticated) {
5276
return false;
5377
}
5478

5579
return this.args.topEntries.some((entry) => entry.user.id === this.authenticator.currentUserId);
5680
}
81+
82+
loadUserSpecificResourcesTask = task({ keepLatest: true }, async (): Promise<void> => {
83+
if (!this.userIsInTopLeaderboardEntries) {
84+
this.surroundingEntries = (await this.store.query('leaderboard-entry', {
85+
include: 'leaderboard,user',
86+
leaderboard_id: this.args.language.leaderboard!.id,
87+
user_id: this.authenticator.currentUserId, // Only used in tests since mirage doesn't have auth context
88+
filter_type: 'around_me',
89+
})) as unknown as LeaderboardEntryModel[];
90+
91+
const userRankCalculations = (await this.store.query('leaderboard-rank-calculation', {
92+
include: 'user',
93+
leaderboard_id: this.args.language.leaderboard!.id,
94+
user_id: this.authenticator.currentUserId, // Only used in tests since mirage doesn't have auth context
95+
})) as unknown as LeaderboardRankCalculationModel[];
96+
97+
this.userRankCalculation = userRankCalculations[0] || null;
98+
99+
// TODO: Also look at "outdated" user rank calculations?
100+
if (!this.userRankCalculation) {
101+
this.userRankCalculation = await this.store
102+
.createRecord('leaderboard-rank-calculation', {
103+
leaderboard: this.args.language.leaderboard!,
104+
user: this.authenticator.currentUser!,
105+
})
106+
.save();
107+
}
108+
}
109+
});
57110
}
58111

59112
declare module '@glint/environment-ember-loose/registry' {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<tr class="bg-gray-50">
22
<td colspan="4" class="px-4 py-3 whitespace-nowrap border border-gray-200 text-center">
33
<div class="text-xs text-gray-500">
4-
... other users ...
4+
{{@text}}
55
</div>
66
</td>
77
</tr>

app/components/leaderboard-page/entries-table/filler-row.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import Component from '@glimmer/component';
22

33
interface Signature {
44
Element: HTMLTableRowElement;
5+
6+
Args: {
7+
text: string;
8+
};
59
}
610

711
export default class LeaderboardPageEntriesTableFillerRow extends Component<Signature> {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Model, { attr, belongsTo } from '@ember-data/model';
2+
import type LeaderboardModel from './leaderboard';
3+
import type UserModel from './user';
4+
5+
export default class LeaderboardRankCalculationModel extends Model {
6+
@belongsTo('leaderboard', { async: false, inverse: null }) declare leaderboard: LeaderboardModel;
7+
@belongsTo('user', { async: false, inverse: 'leaderboardRankCalculations' }) declare user: UserModel;
8+
9+
@attr('date') declare calculatedAt: Date;
10+
@attr('number') declare rank: number;
11+
}

app/models/user.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import AffiliateEarningsPayoutModel from 'codecrafters-frontend/models/affiliate-earnings-payout';
12
import AffiliateLinkModel from 'codecrafters-frontend/models/affiliate-link';
23
import AffiliateReferralModel from 'codecrafters-frontend/models/affiliate-referral';
34
import BadgeAwardModel from 'codecrafters-frontend/models/badge-award';
45
import ConceptEngagementModel from 'codecrafters-frontend/models/concept-engagement';
5-
import config from 'codecrafters-frontend/config/environment';
66
import CourseExtensionIdeaVoteModel from 'codecrafters-frontend/models/course-extension-idea-vote';
77
import CourseIdeaVoteModel from 'codecrafters-frontend/models/course-idea-vote';
88
import CourseLanguageRequestModel from 'codecrafters-frontend/models/course-language-request';
@@ -16,15 +16,16 @@ import GitHubAppInstallationModel from 'codecrafters-frontend/models/github-app-
1616
import InstitutionMembershipGrantApplicationModel from 'codecrafters-frontend/models/institution-membership-grant-application';
1717
import InstitutionMembershipGrantModel from 'codecrafters-frontend/models/institution-membership-grant';
1818
import InvoiceModel from 'codecrafters-frontend/models/invoice';
19+
import LeaderboardRankCalculationModel from './leaderboard-rank-calculation';
1920
import Model, { attr, hasMany } from '@ember-data/model';
21+
import PromotionalDiscountModel from 'codecrafters-frontend/models/promotional-discount';
2022
import ReferralActivationModel from 'codecrafters-frontend/models/referral-activation';
21-
import AffiliateEarningsPayoutModel from 'codecrafters-frontend/models/affiliate-earnings-payout';
2223
import ReferralLinkModel from 'codecrafters-frontend/models/referral-link';
2324
import RepositoryModel from 'codecrafters-frontend/models/repository';
24-
import PromotionalDiscountModel from 'codecrafters-frontend/models/promotional-discount';
2525
import SubscriptionModel from 'codecrafters-frontend/models/subscription';
2626
import TeamMembershipModel from 'codecrafters-frontend/models/team-membership';
2727
import UserProfileEventModel from 'codecrafters-frontend/models/user-profile-event';
28+
import config from 'codecrafters-frontend/config/environment';
2829
import { collectionAction, memberAction } from 'ember-api-actions';
2930
import { inject as service } from '@ember/service';
3031

@@ -70,6 +71,7 @@ export default class UserModel extends Model {
7071
@hasMany('institution-membership-grant', { async: false, inverse: 'user' })
7172
institutionMembershipGrants!: InstitutionMembershipGrantModel[];
7273

74+
@hasMany('leaderboard-rank-calculation', { async: false, inverse: 'user' }) leaderboardRankCalculations!: LeaderboardRankCalculationModel[];
7375
@hasMany('referral-activation', { async: false, inverse: 'customer' }) referralActivationsAsCustomer!: ReferralActivationModel[];
7476
@hasMany('referral-activation', { async: false, inverse: 'referrer' }) referralActivationsAsReferrer!: ReferralActivationModel[];
7577
@hasMany('affiliate-earnings-payout', { async: false, inverse: 'user' }) affiliateEarningsPayouts!: AffiliateEarningsPayoutModel[];

app/routes/contest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export default class ContestRoute extends BaseRoute {
137137
include: 'leaderboard,user',
138138
leaderboard_id: model.contest.leaderboard.id,
139139
filter_type: 'around_me',
140+
user_id: this.authenticator.currentUserId, // Only used in tests since mirage doesn't have auth context
140141
})) as unknown as LeaderboardEntryModel[];
141142
}
142143
}

app/routes/leaderboard.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type LeaderboardEntryModel from 'codecrafters-frontend/models/leaderboard
1111
export type ModelType = {
1212
language: LanguageModel;
1313
leaderboard: LeaderboardModel;
14-
surroundingLeaderboardEntries: LeaderboardEntryModel[];
1514
topLeaderboardEntries: LeaderboardEntryModel[];
1615
};
1716

@@ -37,16 +36,6 @@ export default class LeaderboardRoute extends BaseRoute {
3736

3837
const language = languages.find((language) => language.slug === params.language_slug)!;
3938

40-
let surroundingLeaderboardEntries: LeaderboardEntryModel[] = [];
41-
42-
if (this.authenticator.isAuthenticated) {
43-
surroundingLeaderboardEntries = (await this.store.query('leaderboard-entry', {
44-
include: 'leaderboard,user',
45-
leaderboard_id: language.leaderboard!.id,
46-
filter_type: 'around_me',
47-
})) as unknown as LeaderboardEntryModel[];
48-
}
49-
5039
const topLeaderboardEntries = (await this.store.query('leaderboard-entry', {
5140
include: 'leaderboard,user',
5241
leaderboard_id: language.leaderboard!.id,
@@ -56,7 +45,6 @@ export default class LeaderboardRoute extends BaseRoute {
5645
return {
5746
language: language,
5847
leaderboard: language.leaderboard!,
59-
surroundingLeaderboardEntries: surroundingLeaderboardEntries,
6048
topLeaderboardEntries: topLeaderboardEntries,
6149
};
6250
}

app/templates/leaderboard.hbs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66
</RoadmapInfoAlert> }}
77

88
<div class="flex items-start gap-8">
9-
<LeaderboardPage::EntriesTable
10-
@language={{@model.language}}
11-
@surroundingEntries={{@model.surroundingLeaderboardEntries}}
12-
@topEntries={{@model.topLeaderboardEntries}}
13-
class="flex-grow"
14-
/>
9+
<LeaderboardPage::EntriesTable @language={{@model.language}} @topEntries={{@model.topLeaderboardEntries}} class="flex-grow" />
1510
</div>
1611
</div>

mirage/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import institutionMembershipGrantApplications from './handlers/institution-membe
4343
import institutions from './handlers/institutions';
4444
import languages from './handlers/languages';
4545
import leaderboardEntries from './handlers/leaderboard-entries';
46+
import leaderboardRankCalculations from './handlers/leaderboard-rank-calculations';
4647
import logstreams from './handlers/logstreams';
4748
import onboardingSurveys from './handlers/onboarding-surveys';
4849
import perks from './handlers/perks';
@@ -181,6 +182,7 @@ function routes() {
181182
institutions(this);
182183
languages(this);
183184
leaderboardEntries(this);
185+
leaderboardRankCalculations(this);
184186
logstreams(this);
185187
onboardingSurveys(this);
186188
perks(this);

0 commit comments

Comments
 (0)