Skip to content

Commit 978878c

Browse files
authored
feat(account page): change test activity graph starting day depending on the browser locale (@fehmer) (monkeytypegame#6385)
implements monkeytypegame#6356
1 parent 821478e commit 978878c

File tree

9 files changed

+626
-75
lines changed

9 files changed

+626
-75
lines changed

frontend/__tests__/elements/test-activity-calendar.spec.ts

Lines changed: 400 additions & 33 deletions
Large diffs are not rendered by default.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as DateAndTime from "../../src/ts/utils/date-and-time";
2+
3+
describe("date-and-time", () => {
4+
const testCases = [
5+
{ locale: "en-US", firstDayOfWeek: 0 },
6+
{ locale: "en", firstDayOfWeek: 0 },
7+
{ locale: "de-DE", firstDayOfWeek: 1 },
8+
{ locale: "en-DE", firstDayOfWeek: 1, firefoxFirstDayOfWeek: 0 },
9+
{ locale: "de-AT", firstDayOfWeek: 1 },
10+
{ locale: "ps-AF", firstDayOfWeek: 6, firefoxFirstDayOfWeek: 0 },
11+
{ locale: "de-unknown", firstDayOfWeek: 1 },
12+
{ locale: "xx-yy", firstDayOfWeek: 1, firefoxFirstDayOfWeek: 0 },
13+
];
14+
15+
describe("getFirstDayOfTheWeek", () => {
16+
const languageMock = vi.spyOn(window.navigator, "language", "get");
17+
const localeMock = vi.spyOn(Intl, "Locale");
18+
19+
beforeEach(() => {
20+
languageMock.mockReset();
21+
localeMock.mockReset();
22+
});
23+
24+
it("fallback to sunday for missing language", () => {
25+
//GIVEN
26+
languageMock.mockReturnValue(null as any);
27+
28+
//WHEN / THEN
29+
expect(DateAndTime.getFirstDayOfTheWeek()).toEqual(0);
30+
});
31+
32+
describe("with weekInfo", () => {
33+
it.for(testCases)(`$locale`, ({ locale, firstDayOfWeek }) => {
34+
//GIVEN
35+
languageMock.mockReturnValue(locale);
36+
localeMock.mockImplementationOnce(
37+
() => ({ weekInfo: { firstDay: firstDayOfWeek } } as any)
38+
);
39+
40+
//WHEN/THEN
41+
expect(DateAndTime.getFirstDayOfTheWeek()).toEqual(firstDayOfWeek);
42+
});
43+
});
44+
45+
describe("with getWeekInfo", () => {
46+
it("with getWeekInfo on monday", () => {
47+
languageMock.mockReturnValue("en-US");
48+
localeMock.mockImplementationOnce(
49+
() => ({ getWeekInfo: () => ({ firstDay: 1 }) } as any)
50+
);
51+
52+
//WHEN/THEN
53+
expect(DateAndTime.getFirstDayOfTheWeek()).toEqual(1);
54+
});
55+
it("with getWeekInfo on sunday", () => {
56+
languageMock.mockReturnValue("en-US");
57+
localeMock.mockImplementationOnce(
58+
() => ({ getWeekInfo: () => ({ firstDay: 7 }) } as any)
59+
);
60+
61+
//WHEN/THEN
62+
expect(DateAndTime.getFirstDayOfTheWeek()).toEqual(0);
63+
});
64+
});
65+
66+
describe("without weekInfo (firefox)", () => {
67+
beforeEach(() => {
68+
localeMock.mockImplementationOnce(() => ({} as any));
69+
});
70+
71+
it.for(testCases)(
72+
`$locale`,
73+
({ locale, firstDayOfWeek, firefoxFirstDayOfWeek }) => {
74+
//GIVEN
75+
languageMock.mockReturnValue(locale);
76+
77+
//WHEN/THEN
78+
expect(DateAndTime.getFirstDayOfTheWeek()).toEqual(
79+
firefoxFirstDayOfWeek !== undefined
80+
? firefoxFirstDayOfWeek
81+
: firstDayOfWeek
82+
);
83+
}
84+
);
85+
});
86+
});
87+
});

frontend/__tests__/vitest.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Assertion, AsymmetricMatchersContaining } from "vitest";
2+
import { TestActivityDay } from "../src/ts/elements/test-activity-calendar";
23

3-
interface ActivityDayMatchers<R = MonkeyTypes.TestActivityDay> {
4+
interface ActivityDayMatchers<R = TestActivityDay> {
45
toBeDate: (date: string) => ActivityDayMatchers<R>;
56
toHaveTests: (tests: number) => ActivityDayMatchers<R>;
67
toHaveLevel: (level?: string | number) => ActivityDayMatchers<R>;

frontend/src/html/pages/account.html

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -309,24 +309,8 @@
309309
</div>
310310
<div class="activity"></div>
311311
<div class="months"></div>
312-
<div class="daysFull">
313-
<div></div>
314-
<div><div class="text">monday</div></div>
315-
<div></div>
316-
<div><div class="text">wednesday</div></div>
317-
<div></div>
318-
<div><div class="text">friday</div></div>
319-
<div></div>
320-
</div>
321-
<div class="days">
322-
<div></div>
323-
<div><div class="text">mon</div></div>
324-
<div></div>
325-
<div><div class="text">wed</div></div>
326-
<div></div>
327-
<div><div class="text">fri</div></div>
328-
<div></div>
329-
</div>
312+
<div class="daysFull"></div>
313+
<div class="days"></div>
330314
<div class="nodata hidden">No data found.</div>
331315
</div>
332316
</div>

frontend/src/styles/account.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -522,10 +522,6 @@
522522
font-size: var(--font-size);
523523
}
524524

525-
.days {
526-
display: none;
527-
}
528-
529525
.daysFull {
530526
margin-right: 2rem;
531527
}
@@ -551,6 +547,10 @@
551547
}
552548
}
553549

550+
.days {
551+
display: none;
552+
}
553+
554554
.nodata {
555555
grid-area: chart;
556556
}

frontend/src/ts/db.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import {
2929
} from "./constants/default-snapshot";
3030
import { getDefaultConfig } from "./constants/default-config";
3131
import { FunboxMetadata } from "../../../packages/funbox/src/types";
32+
import { getFirstDayOfTheWeek } from "./utils/date-and-time";
3233

3334
let dbSnapshot: Snapshot | undefined;
35+
const firstDayOfTheWeek = getFirstDayOfTheWeek();
3436

3537
export class SnapshotInitError extends Error {
3638
constructor(message: string, public responseCode: number) {
@@ -163,7 +165,8 @@ export async function initSnapshot(): Promise<Snapshot | number | boolean> {
163165
if (userData.testActivity !== undefined) {
164166
snap.testActivity = new ModifiableTestActivityCalendar(
165167
userData.testActivity.testsByDays,
166-
new Date(userData.testActivity.lastDay)
168+
new Date(userData.testActivity.lastDay),
169+
firstDayOfTheWeek
167170
);
168171
}
169172

@@ -1055,6 +1058,7 @@ export async function getTestActivityCalendar(
10551058
dbSnapshot.testActivityByYear[year] = new TestActivityCalendar(
10561059
testsByDays,
10571060
lastDay,
1061+
firstDayOfTheWeek,
10581062
true
10591063
);
10601064
}

frontend/src/ts/elements/test-activity-calendar.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import {
1111
startOfYear,
1212
differenceInWeeks,
1313
startOfMonth,
14-
nextSunday,
15-
previousSunday,
16-
isSunday,
17-
nextSaturday,
18-
isSaturday,
1914
subWeeks,
2015
Interval,
16+
toDate,
17+
previousDay,
18+
Day,
19+
nextDay,
2120
} from "date-fns";
2221

23-
type TestActivityDay = {
22+
export type TestActivityDay = {
2423
level: string;
2524
label?: string;
2625
};
@@ -35,12 +34,15 @@ export class TestActivityCalendar implements TestActivityCalendar {
3534
protected startDay: Date;
3635
protected endDay: Date;
3736
protected isFullYear: boolean;
37+
public firstDayOfWeek: Day;
3838

3939
constructor(
4040
data: (number | null | undefined)[],
4141
lastDay: Date,
42+
firstDayOfWeek: Day,
4243
fullYear = false
4344
) {
45+
this.firstDayOfWeek = firstDayOfWeek;
4446
const local = new UTCDateMini(lastDay);
4547
const interval = this.getInterval(local, fullYear);
4648

@@ -56,7 +58,9 @@ export class TestActivityCalendar implements TestActivityCalendar {
5658
if (!fullYear) {
5759
//show the last 52 weeks. Not using one year to avoid the graph to show 54 weeks
5860
start = addDays(subWeeks(end, 52), 1);
59-
if (!isSunday(start)) start = previousSunday(start);
61+
if (!this.isFirstDayOfWeek(start)) {
62+
start = this.previousFirstDayOfWeek(start);
63+
}
6064
}
6165

6266
return { start, end };
@@ -91,11 +95,14 @@ export class TestActivityCalendar implements TestActivityCalendar {
9195
let start = i === 0 ? this.startDay : startOfMonth(month);
9296
let end = i === months.length - 1 ? this.endDay : endOfMonth(start);
9397

94-
if (!isSunday(start)) {
95-
start = (i === 0 ? previousSunday : nextSunday)(start);
98+
if (!this.isFirstDayOfWeek(start)) {
99+
start =
100+
i === 0
101+
? this.previousFirstDayOfWeek(start)
102+
: this.nextFirstDayOfWeek(start);
96103
}
97-
if (!isSaturday(end)) {
98-
end = nextSaturday(end);
104+
if (!this.isLastDayOfWeek(end)) {
105+
end = this.nextLastDayOfWeek(end);
99106
}
100107

101108
const weeks = differenceInWeeks(end, start, { roundingMethod: "ceil" });
@@ -124,7 +131,7 @@ export class TestActivityCalendar implements TestActivityCalendar {
124131
};
125132

126133
//skip weekdays in the previous month
127-
for (let i = 0; i < this.startDay.getDay(); i++) {
134+
for (let i = 0; i < this.startDay.getDay() - this.firstDayOfWeek; i++) {
128135
result.push({
129136
level: "filler",
130137
});
@@ -148,7 +155,7 @@ export class TestActivityCalendar implements TestActivityCalendar {
148155
}
149156

150157
//add weekdays missing
151-
for (let i = this.endDay.getDay(); i < 6; i++) {
158+
for (let i = this.endDay.getDay() - this.firstDayOfWeek; i < 6; i++) {
152159
result.push({
153160
level: "filler",
154161
});
@@ -178,6 +185,22 @@ export class TestActivityCalendar implements TestActivityCalendar {
178185
const mid = sum / trimmed.length;
179186
return [Math.floor(mid / 2), Math.round(mid), Math.round(mid * 1.5)];
180187
}
188+
189+
private isFirstDayOfWeek(date: Date): boolean {
190+
return toDate(date).getDay() === this.firstDayOfWeek;
191+
}
192+
private previousFirstDayOfWeek(date: Date): Date {
193+
return previousDay(date, this.firstDayOfWeek);
194+
}
195+
private nextFirstDayOfWeek(date: Date): Date {
196+
return nextDay(date, this.firstDayOfWeek);
197+
}
198+
private isLastDayOfWeek(date: Date): boolean {
199+
return toDate(date).getDay() === (this.firstDayOfWeek + 6) % 7;
200+
}
201+
private nextLastDayOfWeek(date: Date): Date {
202+
return nextDay(date, ((this.firstDayOfWeek + 6) % 7) as Day);
203+
}
181204
}
182205

183206
export class ModifiableTestActivityCalendar
@@ -186,8 +209,8 @@ export class ModifiableTestActivityCalendar
186209
{
187210
private lastDay: Date;
188211

189-
constructor(data: (number | null)[], lastDay: Date) {
190-
super(data, lastDay);
212+
constructor(data: (number | null)[], lastDay: Date, firstDayOfWeek: Day) {
213+
super(data, lastDay, firstDayOfWeek);
191214
this.lastDay = new UTCDateMini(lastDay);
192215
}
193216

@@ -218,9 +241,14 @@ export class ModifiableTestActivityCalendar
218241
getFullYearCalendar(): TestActivityCalendar {
219242
const today = new Date();
220243
if (this.lastDay.getFullYear() !== new UTCDateMini(today).getFullYear()) {
221-
return new TestActivityCalendar([], today, true);
244+
return new TestActivityCalendar([], today, this.firstDayOfWeek, true);
222245
} else {
223-
return new TestActivityCalendar(this.data, this.lastDay, true);
246+
return new TestActivityCalendar(
247+
this.data,
248+
this.lastDay,
249+
this.firstDayOfWeek,
250+
true
251+
);
224252
}
225253
}
226254
}

frontend/src/ts/elements/test-activity.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function init(
2222

2323
yearSelector = getYearSelector();
2424
initYearSelector("current", userSignUpDate?.getFullYear() || 2022);
25+
updateLabels(calendar.firstDayOfWeek);
2526
update(calendar);
2627
}
2728

@@ -128,3 +129,40 @@ function getYearSelector(): SlimSelect {
128129
});
129130
return yearSelector;
130131
}
132+
133+
const daysDisplay = [
134+
"sunday",
135+
"monday",
136+
"tuesday",
137+
"wednesday",
138+
"thursday",
139+
"friday",
140+
"saturday",
141+
];
142+
function updateLabels(firstDayOfWeek: number): void {
143+
const days: (string | undefined)[] = [];
144+
for (let i = 0; i < 7; i++) {
145+
days.push(
146+
i % 2 != firstDayOfWeek % 2
147+
? daysDisplay[(firstDayOfWeek + i) % 7]
148+
: undefined
149+
);
150+
}
151+
152+
const buildHtml = (maxLength?: number): string => {
153+
const shorten =
154+
maxLength !== undefined
155+
? (it: string) => it.substring(0, maxLength)
156+
: (it: string) => it;
157+
return days
158+
.map((it) =>
159+
it !== undefined
160+
? `<div><div class="text">${shorten(it)}</div></div>`
161+
: "<div></div>"
162+
)
163+
.join("");
164+
};
165+
166+
$("#testActivity .daysFull").html(buildHtml());
167+
$("#testActivity .days").html(buildHtml(3));
168+
}

0 commit comments

Comments
 (0)