Skip to content

Commit d08bc73

Browse files
authored
Merge pull request #2917 from codecrafters-io/arpan/cc-1825-randomize-voting-list-order-per-user-per-week
Change Roadmap Idea Ordering
2 parents 205b0d7 + 0b6c605 commit d08bc73

File tree

13 files changed

+345
-26
lines changed

13 files changed

+345
-26
lines changed

app/controllers/roadmap/course-extension-ideas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default class CourseExtensionIdeasController extends Controller {
2222
@service declare store: Store;
2323

2424
get orderedCourseExtensionIdeas() {
25-
return this.model.courseExtensionIdeas.filterBy('course', this.selectedCourse).sortBy('reverseSortPositionForRoadmapPage').reverse();
25+
return this.model.courseExtensionIdeas.filterBy('course', this.selectedCourse).sortBy('sortPositionForRoadmapPage');
2626
}
2727

2828
get orderedCourses() {

app/controllers/roadmap/course-ideas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export default class CourseIdeasController extends Controller {
1111
};
1212

1313
get orderedCourseIdeas() {
14-
return this.model.courseIdeas.rejectBy('isArchived').sortBy('reverseSortPositionForRoadmapPage').reverse();
14+
return this.model.courseIdeas.rejectBy('isArchived').sortBy('sortPositionForRoadmapPage');
1515
}
1616
}

app/models/course-extension-idea.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { memberAction } from 'ember-api-actions';
44
import type CourseModel from './course';
55
import type CourseExtensionIdeaVoteModel from './course-extension-idea-vote';
66
import type AuthenticatorService from 'codecrafters-frontend/services/authenticator';
7+
import type DateService from 'codecrafters-frontend/services/date';
8+
import { getSortPositionForRoadmapPage } from 'codecrafters-frontend/utils/roadmap-sorting';
79

810
export default class CourseExtensionIdeaModel extends Model {
911
@belongsTo('course', { async: false, inverse: 'extensionIdeas' }) declare course: CourseModel;
@@ -18,6 +20,9 @@ export default class CourseExtensionIdeaModel extends Model {
1820
@attr('number') declare votesCount: number;
1921

2022
@service declare authenticator: AuthenticatorService;
23+
@service declare date: DateService;
24+
25+
private _cachedSortPosition: string | null = null;
2126

2227
get developmentStatusIsInProgress() {
2328
return this.developmentStatus === 'in_progress';
@@ -36,14 +41,18 @@ export default class CourseExtensionIdeaModel extends Model {
3641
return this.createdAt > new Date(Date.now() - 30 * 60 * 60 * 24 * 1000) || this.votesCount < 20;
3742
}
3843

39-
get reverseSortPositionForRoadmapPage(): string {
40-
const reverseSortPositionFromDevelopmentStatus = {
41-
not_started: 3,
42-
in_progress: 2,
43-
released: 1,
44-
}[this.developmentStatus];
44+
get sortPositionForRoadmapPage(): string {
45+
if (this._cachedSortPosition === null) {
46+
this._cachedSortPosition = getSortPositionForRoadmapPage(
47+
this.developmentStatus,
48+
this.votesCount,
49+
this.id,
50+
this.date.now(),
51+
this.authenticator.currentUserId,
52+
);
53+
}
4554

46-
return `${reverseSortPositionFromDevelopmentStatus}-${this.createdAt.toISOString()}`;
55+
return this._cachedSortPosition;
4756
}
4857

4958
async vote() {

app/models/course-idea.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import AuthenticatorService from 'codecrafters-frontend/services/authenticator';
22
import CourseIdeaVoteModel from 'codecrafters-frontend/models/course-idea-vote';
3+
import DateService from 'codecrafters-frontend/services/date';
34
import Model from '@ember-data/model';
45
import { type SyncHasMany, attr, hasMany } from '@ember-data/model';
56
import { equal } from '@ember/object/computed'; // eslint-disable-line ember/no-computed-properties-in-native-classes
67
import { memberAction } from 'ember-api-actions';
78
import { inject as service } from '@ember/service';
9+
import { getSortPositionForRoadmapPage } from 'codecrafters-frontend/utils/roadmap-sorting';
810

911
export default class CourseIdeaModel extends Model {
1012
@hasMany('course-idea-vote', { async: false, inverse: 'courseIdea' }) declare currentUserVotes: SyncHasMany<CourseIdeaVoteModel>;
@@ -22,20 +24,27 @@ export default class CourseIdeaModel extends Model {
2224
@equal('developmentStatus', 'released') declare developmentStatusIsReleased: boolean;
2325

2426
@service declare authenticator: AuthenticatorService;
27+
@service declare date: DateService;
28+
29+
private _cachedSortPosition: string | null = null;
2530

2631
get isNewlyCreated(): boolean {
2732
// 30 days or less old or less than 100 votes
2833
return this.createdAt > new Date(Date.now() - 30 * 60 * 60 * 24 * 1000) || this.votesCount < 100;
2934
}
3035

31-
get reverseSortPositionForRoadmapPage(): string {
32-
const reverseSortPositionFromDevelopmentStatus = {
33-
not_started: 3,
34-
in_progress: 2,
35-
released: 1,
36-
}[this.developmentStatus];
36+
get sortPositionForRoadmapPage(): string {
37+
if (this._cachedSortPosition === null) {
38+
this._cachedSortPosition = getSortPositionForRoadmapPage(
39+
this.developmentStatus,
40+
this.votesCount,
41+
this.id,
42+
this.date.now(),
43+
this.authenticator.currentUserId,
44+
);
45+
}
3746

38-
return `${reverseSortPositionFromDevelopmentStatus}-${this.createdAt.toISOString()}`;
47+
return this._cachedSortPosition;
3948
}
4049

4150
async vote(): Promise<CourseIdeaVoteModel> {

app/utils/roadmap-sorting.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import seedrandom from 'seedrandom';
2+
3+
export function getRoughWeekSeed(date: number): number {
4+
const weekMs = 7 * 24 * 60 * 60 * 1000; // milliseconds per week
5+
6+
return date - (date % weekMs);
7+
}
8+
9+
export function getSortPositionForRoadmapPage(
10+
developmentStatus: string,
11+
votesCount: number,
12+
id: string,
13+
date: number,
14+
currentUserId?: string | null,
15+
): string {
16+
const statusSortKey =
17+
{
18+
in_progress: '1',
19+
not_started: '2',
20+
released: '3',
21+
}[developmentStatus] ?? '4'; // Default to lowest priority if status is unknown
22+
23+
const sortKeys: string[] = [statusSortKey];
24+
25+
// Special case: logged-in user and not_started
26+
if (currentUserId && developmentStatus === 'not_started') {
27+
// Not started: sort by random but fixed per user per week
28+
const weekSeed = getRoughWeekSeed(date);
29+
const userWeekSeed = `${currentUserId}-${weekSeed}`;
30+
const rng = seedrandom(`${userWeekSeed}-${id}`);
31+
const randomSeed = Math.floor(rng() * 1000000); // Generate a number between 0-999999
32+
const paddedRandomSeed = randomSeed.toString().padStart(10, '0');
33+
sortKeys.push(paddedRandomSeed);
34+
} else {
35+
// All other cases: sort by vote count descending (highest votes first)
36+
const invertedVoteCountKey = (999999999 - votesCount).toString().padStart(10, '0');
37+
sortKeys.push(invertedVoteCountKey);
38+
}
39+
40+
return sortKeys.join('-');
41+
}

mirage/data/course-extension-ideas.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,36 @@ export default [
3030
development_status: 'not_started',
3131
description_md: 'This is the first extension idea for the dummy course',
3232
},
33+
{
34+
slug: 'redis-resp3-protocol',
35+
course_slug: 'redis',
36+
name: 'RESP3 Protocol',
37+
development_status: 'in_progress',
38+
description_md:
39+
"In this challenge extension you'll add support for the [RESP3 protocol](https://redis.io/docs/reference/protocol-spec/) to your Redis implementation. Along the way you'll learn about how Redis clients 'upgrade' to RESP3 using the HELLO command, newer data types that RESP3 offers and more.",
40+
},
41+
{
42+
slug: 'redis-pubsub',
43+
course_slug: 'redis',
44+
name: 'Pub/Sub',
45+
development_status: 'not_started',
46+
description_md:
47+
"In this challenge extension you'll add support for [Pub/Sub](https://redis.io/docs/interact/pubsub/) to your Redis implementation. Along the way you'll learn about the PUBLISH command, the SUBSCRIBE command and more.",
48+
},
49+
{
50+
slug: 'redis-lists',
51+
course_slug: 'redis',
52+
name: 'Lists',
53+
development_status: 'not_started',
54+
description_md:
55+
"In this challenge extension you'll add support for the [List](https://redis.io/docs/data-types/lists/) data type to your Redis implementation. Along the way you'll learn about commands like LPUSH, RPOP and more.",
56+
},
57+
{
58+
slug: 'redis-replication',
59+
course_slug: 'redis',
60+
name: 'Replication',
61+
development_status: 'not_started',
62+
description_md:
63+
"In this challenge extension you'll add support for [Replication](https://redis.io/docs/management/replication/) to your Redis implementation. Along the way you'll learn about how Redis's leader-follower replication works, the WAIT command, the PSYNC command and more.",
64+
},
3365
];

mirage/data/course-ideas.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ export default [
99
{
1010
slug: 'http',
1111
name: 'Build your own HTTP Server',
12-
development_status: 'in-progress',
12+
development_status: 'in_progress',
1313
description_md:
1414
"[HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) is the protocol that powers the web. In this challenge, you'll build a HTTP/1.1 server that is capable of serving multiple clients. \n\nAlong the way you'll learn about TCP servers, [HTTP request syntax](https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html), HTTP/1.1's [request pipelining](https://en.wikipedia.org/wiki/HTTP_pipelining) and more.",
1515
},
1616
{
1717
slug: 'shell',
1818
name: 'Build your own Shell',
19-
development_status: 'not-started',
19+
development_status: 'not_started',
2020
description_md:
2121
"[Shells](https://en.wikipedia.org/wiki/Unix_shell) are a command-line interface to your operating system. In this challenge, you'll build your own version of `bash` that is capable of executing basic shell commands. \n\nAlong the way, you'll learn about [pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix)), [redirection](https://en.wikipedia.org/wiki/Redirection_(computing)), [fork+exec](https://en.wikipedia.org/wiki/Fork%E2%80%93exec) and more.",
2222
},
@@ -31,14 +31,14 @@ export default [
3131
{
3232
slug: 'bittorrent',
3333
name: 'Build your own BitTorrent client',
34-
development_status: 'not-started',
34+
development_status: 'not_started',
3535
description_md:
3636
"[BitTorrent](https://en.wikipedia.org/wiki/BitTorrent) is a communication protocol for P2P file sharing. In this challenge, you'll build a BitTorrent client that is capable of downloading a publicly available file using the BitTorrent protocol. \n\nAlong the way you'll learn about the [BitTorrent protocol](https://www.bittorrent.org/beps/bep_0003.html), [trackers](https://www.bittorrent.org/beps/bep_0003.html#trackers), seeds & peers, file segments and more.",
3737
},
3838
{
3939
slug: 'react',
4040
name: 'Build your own React',
41-
development_status: 'not-started',
41+
development_status: 'not_started',
4242
description_md:
4343
"[React](https://reactjs.org/) is a Javascript framework for building user interfaces. In this challenge, you'll build a React implementation that supports [function components](https://reactjs.org/docs/components-and-props.html#function-and-class-components) and [hooks](https://reactjs.org/docs/hooks-intro.html). \n\nAlong the way, you'll learn about React's [API](https://reactjs.org/docs/react-api.html), [DOM-diffing algorithm](https://reactjs.org/docs/reconciliation.html#the-diffing-algorithm), [hooks](https://reactjs.org/docs/hooks-intro.html) and more!",
4444
},

mirage/utils/create-course-ideas.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default function createCourseIdeas(server) {
66
createdAt: new Date(),
77
descriptionMarkdown: courseIdeaData.description_md,
88
developmentStatus: courseIdeaData.development_status,
9+
isArchived: courseIdeaData.is_archived || false,
910
name: courseIdeaData.name,
1011
votesCount: 0,
1112
});

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"@types/qunit": "^2.19.10",
100100
"@types/rails__actioncable": "^6.1.11",
101101
"@types/rsvp": "^4.0.9",
102+
"@types/seedrandom": "^3.0.8",
102103
"@types/showdown": "^2.0.2",
103104
"@types/three": "^0.172.0",
104105
"@typescript-eslint/eslint-plugin": "^8.35.0",
@@ -217,6 +218,7 @@
217218
"node-html-parser": "^7.0.1",
218219
"player.js": "^0.1.0",
219220
"posthog-js": "^1.234.6",
221+
"seedrandom": "^3.0.5",
220222
"showdown": "^2.1.0",
221223
"three": "^0.172.0",
222224
"web-vitals": "^4.2.4"

0 commit comments

Comments
 (0)