Skip to content

Commit 6b489db

Browse files
committed
feat: measure effort relative to the daily goal
1 parent aa3038a commit 6b489db

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+187
-80
lines changed

packages/keybr-intl/lib/messages/ar.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/bg.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ca.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/cs.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/da.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/de.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/el.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/en.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/eo.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/es.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/et.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/fa.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/fr.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/he.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/hr.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/hu.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/id.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/it.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ja.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ko.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ne.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/nl.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/pl.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/pt-br.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/pt-pt.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ro.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/ru.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/sv.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/th.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/tr.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/uk.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/vi.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/zh-hans.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/lib/messages/zh-hant.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/translations/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
"profile.accuracy.legend": "Above are listed the longest continuous sequences of lessons with accuracy above a given threshold, along with statistics about every such sequence. The longer the sequence of lessons, the better.",
268268
"profile.accuracy.noData": "You don’t have any accuracy streaks. Consider completing a lesson with a highest accuracy possible, regardless of typing speed.",
269269
"profile.calendar.averageSpeed.description": "Average speed: {value}",
270+
"profile.calendar.dailyGoal.description": "Daily goal: {value}",
270271
"profile.calendar.topSpeed.description": "Top speed: {value}",
271272
"profile.calendar.totalLessons.description": "Number of lessons: {value}",
272273
"profile.calendar.totalTime.description": "Time of lessons: {value}",

packages/keybr-lesson-ui/lib/Calendar.module.less

+17-10
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,19 @@
1212
.table {
1313
table-layout: fixed;
1414
border-collapse: collapse;
15+
cursor: default;
1516
}
1617

17-
.caption {
18-
background-color: var(--Calendar-caption__background-color);
19-
text-align: center;
20-
}
21-
18+
.caption,
2219
.headerCell,
2320
.cell {
24-
vertical-align: middle;
25-
inline-size: 1.5rem;
26-
block-size: 1.5rem;
27-
font-size: 0.75rem;
21+
margin: 0;
22+
padding: 0;
23+
}
24+
25+
.caption {
26+
background-color: var(--Calendar-caption__background-color);
2827
text-align: center;
29-
cursor: default;
3028
}
3129

3230
.headerCell {
@@ -36,3 +34,12 @@
3634
.cell {
3735
background-color: var(--Calendar-cell--background-color);
3836
}
37+
38+
.item {
39+
display: block;
40+
inline-size: 1.5rem;
41+
block-size: 1.5rem;
42+
font-size: 0.75rem;
43+
line-height: 1.5rem;
44+
text-align: center;
45+
}

packages/keybr-lesson-ui/lib/Calendar.test.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test } from "node:test";
2+
import { Color } from "@keybr/color";
23
import { FakeIntlProvider } from "@keybr/intl";
34
import { LocalDate, ResultFaker, ResultSummary } from "@keybr/result";
45
import { FakeSettingsContext } from "@keybr/settings";
@@ -16,7 +17,13 @@ test("no results", () => {
1617
const r = render(
1718
<FakeIntlProvider>
1819
<FakeSettingsContext>
19-
<Calendar summary={summary} />
20+
<Calendar
21+
summary={summary}
22+
effort={{
23+
effort: () => 0,
24+
shade: () => Color.parse("#ffffff"),
25+
}}
26+
/>
2027
</FakeSettingsContext>
2128
</FakeIntlProvider>,
2229
);
@@ -40,7 +47,13 @@ test("no results today", () => {
4047
const r = render(
4148
<FakeIntlProvider>
4249
<FakeSettingsContext>
43-
<Calendar summary={summary} />
50+
<Calendar
51+
summary={summary}
52+
effort={{
53+
effort: () => 0,
54+
shade: () => Color.parse("#ffffff"),
55+
}}
56+
/>
4457
</FakeSettingsContext>
4558
</FakeIntlProvider>,
4659
);
@@ -65,7 +78,13 @@ test("render", () => {
6578
const r = render(
6679
<FakeIntlProvider>
6780
<FakeSettingsContext>
68-
<Calendar summary={summary} />
81+
<Calendar
82+
summary={summary}
83+
effort={{
84+
effort: () => 0.5,
85+
shade: () => Color.parse("#ffffff"),
86+
}}
87+
/>
6988
</FakeSettingsContext>
7089
</FakeIntlProvider>,
7190
);

packages/keybr-lesson-ui/lib/Calendar.tsx

+39-31
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import { Color } from "@keybr/color";
1+
import { useIntlNumbers } from "@keybr/intl";
22
import {
33
LocalDate,
44
makeSummaryStats,
55
type Result,
66
type ResultSummary,
7-
type SummaryStats,
87
} from "@keybr/result";
9-
import { useComputedStyles } from "@keybr/themes";
108
import { formatDuration } from "@keybr/widget";
11-
import { type CSSProperties, useMemo } from "react";
129
import { useIntl } from "react-intl";
1310
import * as styles from "./Calendar.module.less";
11+
import { type Effort } from "./effort.ts";
1412
import { useFormatter } from "./format.ts";
1513

16-
export function Calendar({ summary }: { readonly summary: ResultSummary }) {
17-
const effortStyles = useEffortStyles();
14+
export function Calendar({
15+
summary,
16+
effort,
17+
}: {
18+
readonly summary: ResultSummary;
19+
readonly effort: Effort;
20+
}) {
1821
return (
1922
<div className={styles.root}>
2023
{blockList(summary).map((block, index) => (
21-
<Block key={index} block={block} effortStyles={effortStyles} />
24+
<Block key={index} block={block} effort={effort} />
2225
))}
2326
</div>
2427
);
@@ -32,10 +35,10 @@ type BlockData = {
3235

3336
function Block({
3437
block,
35-
effortStyles,
38+
effort,
3639
}: {
3740
readonly block: BlockData;
38-
readonly effortStyles: EffortStyles;
41+
readonly effort: Effort;
3942
}) {
4043
const { formatMessage } = useIntl();
4144

@@ -65,7 +68,7 @@ function Block({
6568
{block.cells.map((row, m) => (
6669
<tr key={m}>
6770
{row.map((cell, n) => (
68-
<Cell key={n} cell={cell} effortStyles={effortStyles} />
71+
<Cell key={n} cell={cell} effort={effort} />
6972
))}
7073
</tr>
7174
))}
@@ -82,22 +85,35 @@ type CellData = {
8285

8386
function Cell({
8487
cell,
85-
effortStyles,
88+
effort,
8689
}: {
8790
readonly cell: CellData | null;
88-
readonly effortStyles: EffortStyles;
91+
readonly effort: Effort;
8992
}) {
9093
const { formatMessage } = useIntl();
94+
const { formatPercents } = useIntlNumbers();
9195
const { formatSpeed } = useFormatter();
9296
if (cell == null) {
9397
return <td />;
9498
}
9599
const { results } = cell;
96100
if (results.length === 0) {
97-
return <td className={styles.cell}>{cell.date.dayOfMonth}</td>;
101+
return (
102+
<td className={styles.cell}>
103+
<span className={styles.item}>{cell.date.dayOfMonth}</span>
104+
</td>
105+
);
98106
}
99107
const stats = makeSummaryStats(results);
108+
const effortValue = effort.effort(stats.time);
100109
const title = [
110+
formatMessage(
111+
{
112+
id: "profile.calendar.dailyGoal.description",
113+
defaultMessage: "Daily goal: {value}",
114+
},
115+
{ value: formatPercents(effortValue) },
116+
),
101117
formatMessage(
102118
{
103119
id: "profile.calendar.totalTime.description",
@@ -128,8 +144,16 @@ function Cell({
128144
),
129145
].join(",\n");
130146
return (
131-
<td className={styles.cell} style={effortStyles(stats)} title={title}>
132-
{cell.date.dayOfMonth}
147+
<td className={styles.cell}>
148+
<span
149+
className={styles.item}
150+
style={{
151+
backgroundColor: String(effort.shade(effortValue)),
152+
}}
153+
title={title}
154+
>
155+
{cell.date.dayOfMonth}
156+
</span>
133157
</td>
134158
);
135159
}
@@ -195,19 +219,3 @@ function blockList(summary: ResultSummary): BlockData[] {
195219
};
196220
}
197221
}
198-
199-
type EffortStyles = (stats: SummaryStats) => CSSProperties;
200-
201-
function useEffortStyles(range = 30 * 60 * 1000): EffortStyles {
202-
const { getPropertyValue } = useComputedStyles();
203-
const color = getPropertyValue("--effort-color") || "#000000";
204-
return useMemo(() => {
205-
return ({ time }: SummaryStats) => {
206-
const style = {} as CSSProperties;
207-
if (range > 0 && time > 0) {
208-
style.backgroundColor = String(Color.parse(color).fade(time / range));
209-
}
210-
return style;
211-
};
212-
}, [range, color]);
213-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useIntlNumbers } from "@keybr/intl";
2+
import { FormattedMessage } from "react-intl";
3+
import { type Effort } from "./effort.ts";
4+
import * as styles from "./EffortLegent.module.less";
5+
6+
export function EffortLegend({ effort }: { readonly effort: Effort }) {
7+
const { formatPercents } = useIntlNumbers();
8+
return (
9+
<>
10+
<FormattedMessage
11+
id="settings.dailyGoal.label"
12+
defaultMessage="Daily goal:"
13+
/>{" "}
14+
{[1.0, 0.75, 0.5, 0.25, 0.0].map((value) => (
15+
<span key={value} className={styles.cell}>
16+
<span
17+
className={styles.item}
18+
style={{ backgroundColor: String(effort.shade(value)) }}
19+
>
20+
{formatPercents(value)}
21+
</span>
22+
</span>
23+
))}
24+
</>
25+
);
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.cell {
2+
display: inline-block;
3+
background-color: var(--Calendar-cell--background-color);
4+
cursor: default;
5+
}
6+
7+
.item {
8+
display: inline-block;
9+
inline-size: 4rem;
10+
padding-block: 0.25rem;
11+
text-align: center;
12+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Color } from "@keybr/color";
2+
import { MutableDailyGoal } from "@keybr/lesson";
3+
import { useSettings } from "@keybr/settings";
4+
import { useComputedStyles } from "@keybr/themes";
5+
import { useMemo } from "react";
6+
7+
export type Effort = {
8+
readonly effort: (time: number) => number;
9+
readonly shade: (effort: number) => Color;
10+
};
11+
12+
export function useEffort(): Effort {
13+
const { settings } = useSettings();
14+
const { getPropertyValue } = useComputedStyles();
15+
const color = getPropertyValue("--effort-color") || "#000000";
16+
return useMemo(() => {
17+
const dailyGoal = new MutableDailyGoal(settings);
18+
const effort = (time: number) => {
19+
return dailyGoal.goal > 0 ? dailyGoal.measure(time) : 1.0;
20+
};
21+
const shade = (effort: number) => {
22+
return Color.parse(color).fade(effort);
23+
};
24+
return { effort, shade };
25+
}, [settings, color]);
26+
}

packages/keybr-lesson-ui/lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export * from "./Calendar.tsx";
22
export * from "./CurrentKey.tsx";
33
export * from "./DailyGoal.tsx";
4+
export * from "./effort.ts";
5+
export * from "./EffortLegend.tsx";
46
export * from "./format.ts";
57
export * from "./gauges.tsx";
68
export * from "./indicators.tsx";

packages/page-profile/lib/profile/CalendarSection.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Calendar } from "@keybr/lesson-ui";
1+
import { Calendar, EffortLegend, useEffort } from "@keybr/lesson-ui";
22
import { type ResultSummary } from "@keybr/result";
33
import { Explainer, Figure } from "@keybr/widget";
44
import { type ReactNode } from "react";
@@ -9,6 +9,8 @@ export function CalendarSection({
99
}: {
1010
readonly summary: ResultSummary;
1111
}): ReactNode {
12+
const effort = useEffort();
13+
1214
return (
1315
<Figure>
1416
<Figure.Caption>
@@ -27,7 +29,11 @@ export function CalendarSection({
2729
</Figure.Description>
2830
</Explainer>
2931

30-
<Calendar summary={summary} />
32+
<Calendar summary={summary} effort={effort} />
33+
34+
<Figure.Legend>
35+
<EffortLegend effort={effort} />
36+
</Figure.Legend>
3137
</Figure>
3238
);
3339
}

0 commit comments

Comments
 (0)