Skip to content

Commit ccd4fe3

Browse files
committed
feat(user): add fetchTopLanguageLeaderboardSlugs API and mirage handler
Add a new member action fetchTopLanguageLeaderboardSlugs to the UserModel to retrieve the top language leaderboard slugs for the current user. This enables fetching the top 3 language leaderboards where the user has the highest scores. Implement a corresponding MirageJS handler to support this endpoint for development and testing, returning sorted and filtered leaderboard slugs based on the current user's leaderboard entries. Fix swapped implementations of fetchCurrent and fetchNextInvoicePreview actions to correctly match their respective API endpoints and return types.
1 parent 8d47e2e commit ccd4fe3

File tree

8 files changed

+40
-50
lines changed

8 files changed

+40
-50
lines changed

app/models/leaderboard-entry.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Model, { attr, belongsTo } from '@ember-data/model';
22
import type LeaderboardModel from './leaderboard';
33
import type UserModel from './user';
44
import CourseModel from './course';
5-
import { collectionAction } from 'ember-api-actions';
65

76
export default class LeaderboardEntryModel extends Model {
87
@belongsTo('leaderboard', { async: false, inverse: 'entries' }) declare leaderboard: LeaderboardModel;
@@ -23,22 +22,4 @@ export default class LeaderboardEntryModel extends Model {
2322

2423
return this.relatedCourseSlugs.map((slug) => allCourses.find((course) => course.slug === slug)).filter(Boolean);
2524
}
26-
27-
declare fetchForCurrentUser: (this: Model, payload: unknown) => Promise<LeaderboardEntryModel[]>;
2825
}
29-
30-
LeaderboardEntryModel.prototype.fetchForCurrentUser = collectionAction({
31-
path: 'for-current-user',
32-
type: 'get',
33-
urlType: 'findAll',
34-
35-
after(response) {
36-
if (response.data) {
37-
this.store.pushPayload(response);
38-
39-
return response.data.map((data: { id: string }) => this.store.peekRecord('leaderboard-entry', data.id));
40-
} else {
41-
return [];
42-
}
43-
},
44-
});

app/models/user.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,41 +226,47 @@ export default class UserModel extends Model {
226226

227227
declare fetchCurrent: (this: Model, payload: unknown) => Promise<UserModel | null>;
228228
declare fetchNextInvoicePreview: (this: Model, payload: unknown) => Promise<InvoiceModel | null>;
229+
declare fetchTopLanguageLeaderboardSlugs: (this: Model, payload: unknown) => Promise<string[]>;
229230
declare syncFeatureFlags: (this: Model, payload: unknown) => Promise<void>;
230231
declare syncUsernameFromGitHub: (this: Model, payload: unknown) => Promise<void>;
231232
}
232233

233-
UserModel.prototype.fetchNextInvoicePreview = memberAction({
234-
path: 'next-invoice-preview',
234+
UserModel.prototype.fetchCurrent = collectionAction({
235+
path: 'current',
235236
type: 'get',
237+
urlType: 'findRecord',
236238

237239
after(response) {
238240
if (response.data) {
239241
this.store.pushPayload(response);
240242

241-
return this.store.peekRecord('invoice', response.data.id);
243+
return this.store.peekRecord('user', response.data.id);
242244
} else {
243245
return null;
244246
}
245247
},
246248
});
247249

248-
UserModel.prototype.fetchCurrent = collectionAction({
249-
path: 'current',
250+
UserModel.prototype.fetchNextInvoicePreview = memberAction({
251+
path: 'next-invoice-preview',
250252
type: 'get',
251-
urlType: 'findRecord',
252253

253254
after(response) {
254255
if (response.data) {
255256
this.store.pushPayload(response);
256257

257-
return this.store.peekRecord('user', response.data.id);
258+
return this.store.peekRecord('invoice', response.data.id);
258259
} else {
259260
return null;
260261
}
261262
},
262263
});
263264

265+
UserModel.prototype.fetchTopLanguageLeaderboardSlugs = memberAction({
266+
path: 'top-language-leaderboard-slugs',
267+
type: 'get',
268+
});
269+
264270
UserModel.prototype.syncFeatureFlags = memberAction({
265271
path: 'sync-feature-flags',
266272
type: 'post',

app/routes/contest.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,16 @@ export default class ContestRoute extends BaseRoute {
111111
}
112112
}
113113

114-
// TODO[Vasyl]: This interacts with the usage in preferredLanguageLeaderboard where we create a temporary leaderboard entry
115-
// Figure out a way to use `query` instead of the adapterOptions strategy? Or maybe more explicit caching?
116-
let topLeaderboardEntries = (await this.store.findAll('leaderboard-entry', {
114+
// TODO[Vasyl]: This has an issue where it can end up picking _any_ leaderboard entry and not just the top ones. We
115+
// don't happen to render contest leaderboards elsewhere so it isn't a problem for now. As a pattern this is something
116+
// I'd like to figure out though. Maybe we use `query` instead of the adapterOptions strategy? Or maybe more explicit caching?
117+
const topLeaderboardEntries = (await this.store.findAll('leaderboard-entry', {
117118
adapterOptions: {
118119
leaderboard_id: contest.leaderboard.id,
119120
},
120121
include: 'user,leaderboard',
121122
})) as unknown as LeaderboardEntryModel[];
122123

123-
topLeaderboardEntries = topLeaderboardEntries.reject((entry) => entry.isNew);
124-
125124
const languages = await this.store.findAll('language');
126125

127126
return {

app/services/preferred-language-leaderboard.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,7 @@ export default class PreferredLanguageLeaderboardService extends Service {
7070

7171
async refresh(): Promise<void> {
7272
await this.authenticator.authenticate();
73-
74-
const userLeaderboardEntries = (await this.store.createRecord('leaderboard-entry').fetchForCurrentUser({
75-
include: 'leaderboard,leaderboard.language,user',
76-
})) as unknown as LeaderboardEntryModel[];
77-
78-
this.preferredLanguageSlugs = userLeaderboardEntries
79-
.filter((entry) => entry.score > 0)
80-
// @ts-expect-error: languageId not defined on LeaderboardModel
81-
.filter((entry) => entry.leaderboard.languageId !== null)
82-
.sort((a, b) => b.score - a.score)
83-
.slice(0, 3)
84-
.map((entry) => entry.leaderboard.language!.slug);
85-
73+
this.preferredLanguageSlugs = (await this.authenticator.currentUser!.fetchTopLanguageLeaderboardSlugs({})).slice(0, 3);
8674
this.localStorage.setItem(PreferredLanguageLeaderboardService.STORAGE_KEY, new StoredData(this.preferredLanguageSlugs, new Date()).toJSON());
8775
}
8876
}

mirage/handlers/leaderboard-entries.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,4 @@ export default function (server) {
3232
throw new Error(`Invalid filter type: ${request.queryParams.filter_type}`);
3333
}
3434
});
35-
36-
server.get('/leaderboard-entries/for-current-user', function (schema) {
37-
return schema.leaderboardEntries.all().filter((leaderboardEntry) => leaderboardEntry.user.id === CurrentMirageUser.currentUserId);
38-
});
3935
}

mirage/handlers/users.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Response } from 'miragejs';
2+
import CurrentMirageUser from 'codecrafters-frontend/mirage/utils/current-mirage-user';
23

34
export default function (server) {
45
server.get('/users', function (schema, request) {
@@ -25,6 +26,25 @@ export default function (server) {
2526
});
2627
});
2728

29+
server.get('/users/:id/top-language-leaderboard-slugs', function (schema) {
30+
if (!CurrentMirageUser.currentUserId) {
31+
return [];
32+
}
33+
34+
const languageLeaderboardIds = schema.languages
35+
.all()
36+
.models.map((language) => language.leaderboard?.id)
37+
.filter(Boolean);
38+
39+
return schema.leaderboardEntries
40+
.all()
41+
.models.filter((leaderboardEntry) => languageLeaderboardIds.includes(leaderboardEntry.leaderboard.id))
42+
.filter((leaderboardEntry) => leaderboardEntry.user.id === CurrentMirageUser.currentUserId)
43+
.sort((a, b) => b.score - a.score)
44+
.slice(0, 3)
45+
.map((entry) => entry.leaderboard.language.slug);
46+
});
47+
2848
server.post('/users/:id/sync-username-from-github', function (schema, request) {
2949
let user = schema.users.find(request.params.id);
3050
user.update({ username: 'updated-username' });

tests/support/api-requests-count.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function apiRequestsCount(server) {
1515
// Triggered on application boot
1616
pathname !== '/api/v1/users/current' &&
1717
// Triggered when header is rendered
18-
pathname !== '/api/v1/leaderboard-entries/for-current-user'
18+
!pathname.match(/^\/api\/v1\/users\/[^/]+\/top-language-leaderboard-slugs$/)
1919
);
2020
});
2121

tests/support/verify-api-requests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function verifyApiRequests(server, expectedRequests) {
1111
// Triggered on application boot
1212
pathname !== '/api/v1/users/current' &&
1313
// Triggered when header is rendered
14-
pathname !== '/api/v1/leaderboard-entries/for-current-user'
14+
!pathname.match(/^\/api\/v1\/users\/[^/]+\/top-language-leaderboard-slugs$/)
1515
);
1616
});
1717

0 commit comments

Comments
 (0)