Merge branch 'master' into feature/friends-list

This commit is contained in:
Christian Fehmer 2025-09-10 14:16:24 +02:00 committed by GitHub
commit 5314e3f572
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 105 additions and 399 deletions

View file

@ -8,6 +8,7 @@ describe("PresetDal", () => {
it("should read", async () => {
//GIVEN
const uid = new ObjectId().toHexString();
const decoyUid = new ObjectId().toHexString();
const first = await PresetDal.addPreset(uid, {
name: "first",
config: { ads: "sellout" },
@ -22,7 +23,11 @@ describe("PresetDal", () => {
showAverage: "off",
},
});
await PresetDal.addPreset("unknown", { name: "unknown", config: {} });
await PresetDal.addPreset(decoyUid, {
name: "unknown",
config: {},
});
//WHEN
const read = await PresetDal.getPresets(uid);
@ -101,7 +106,8 @@ describe("PresetDal", () => {
describe("editPreset", () => {
it("should not fail if preset is unknown", async () => {
await PresetDal.editPreset("uid", {
const uid = new ObjectId().toHexString();
await PresetDal.editPreset(uid, {
_id: new ObjectId().toHexString(),
name: "new",
config: {},
@ -350,8 +356,9 @@ describe("PresetDal", () => {
describe("removePreset", () => {
it("should fail if preset is unknown", async () => {
const uid = new ObjectId().toHexString();
await expect(() =>
PresetDal.removePreset("uid", new ObjectId().toHexString())
PresetDal.removePreset(uid, new ObjectId().toHexString())
).rejects.toThrowError("Preset not found");
});
it("should remove", async () => {
@ -435,7 +442,8 @@ describe("PresetDal", () => {
describe("deleteAllPresets", () => {
it("should not fail if preset is unknown", async () => {
await PresetDal.deleteAllPresets("uid");
const uid = new ObjectId().toHexString();
await PresetDal.deleteAllPresets(uid);
});
it("should delete all", async () => {
//GIVEN

View file

@ -118,7 +118,7 @@ Follow these steps if you want to work on anything involving the database/accoun
## Building and Running Monkeytype
Its time to run Monkeytype. Just like with the databases, you can run the frontend and backend manually or with Docker.
It's time to run Monkeytype. Just like with the databases, you can run the frontend and backend manually or with Docker.
### Dependencies (if running manually)

View file

@ -89,4 +89,4 @@ Once your PR is approved, all that is left to do is merge it!
## Questions
If you have any questions, comments, concerns, or problems, don't hesitate let us know via [email](mailto:jack@monkeytype.com)(to Miodec), on [GitHub](https://github.com/monkeytypegame/monkeytype/discussions) or on [Discord](https://discord.gg/monkeytype) in the [`#development`](https://discord.com/channels/713194177403420752/713196019206324306) channel and a contributor will be happy to assist you.
If you have any questions, comments, concerns, or problems, don't hesitate to let us know via [email](mailto:jack@monkeytype.com)(to Miodec), on [GitHub](https://github.com/monkeytypegame/monkeytype/discussions) or on [Discord](https://discord.gg/monkeytype) in the [`#development`](https://discord.com/channels/713194177403420752/713196019206324306) channel and a contributor will be happy to assist you.

View file

@ -28,7 +28,7 @@ If the language does exist in Monkeytype, but there are no quotes for it create
### Committing Quotes
Once you have added your quotes(s), you now need to create a pull request to the main Monkeytype repository. Go to the branch where you added your quotes on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".
Once you have added your quote(s), you now need to create a pull request to the main Monkeytype repository. Go to the branch where you added your quotes on GitHub. Then make sure your branch is up to date. Once it is up to date, click "contribute".
Update branch:
<img width="1552" alt="Screenshot showing how to update the fork to match the main Monkeytype repository" src="https://user-images.githubusercontent.com/83455454/149186547-5b9fe4fd-b944-4eed-a959-db43f96198bf.png">

View file

@ -1025,113 +1025,6 @@
<button>copy link to clipboard</button>
</div>
</dialog>
<div id="leaderboardsWrapper" class="popupWrapper hidden">
<div id="leaderboards">
<div class="leaderboardsTop">
<div class="mainTitle">All-Time English Leaderboards</div>
<div class="subTitle">Next update in: --:--</div>
<div class="textButton showYesterdayButton hidden">
<i class="fas fa-calendar-day"></i>
<div class="text">Show yesterday</div>
</div>
<div class="buttons">
<div class="buttonGroup timeRange">
<div class="button allTime">all-time</div>
<div class="button daily">daily</div>
<select class="languageSelect" disabled></select>
</div>
</div>
</div>
<div class="tables">
<div class="titleAndTable">
<div class="titleAndButtons">
<div class="title">
Time 15
<i
class="hidden leftTableLoader fas fa-fw fa-spin fa-circle-notch"
></i>
</div>
<div class="buttons">
<div class="button leftTableJumpToTop">
<i class="fas fa-fw fas fa-crown"></i>
<!-- Top -->
</div>
<div class="button leftTableJumpToMe">
<i class="fas fa-fw fas fa-user"></i>
<!-- Me -->
</div>
</div>
</div>
<div class="leftTableWrapper invisible">
<table class="left">
<thead>
<tr>
<td width="1%">#</td>
<td>name</td>
<td class="alignRight" width="1%">
wpm
<br />
<div class="sub">accuracy</div>
</td>
<td class="alignRight" width="1%">
raw
<br />
<div class="sub">consistency</div>
</td>
<td class="alignRight" width="125px">date</td>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
<div class="titleAndTable">
<div class="titleAndButtons">
<div class="title">
Time 60
<i
class="hidden rightTableLoader fas fa-fw fa-spin fa-circle-notch"
></i>
</div>
<div class="buttons">
<div class="button rightTableJumpToTop">
<i class="fas fa-fw fas fa-crown"></i>
<!-- Top -->
</div>
<div class="button rightTableJumpToMe">
<i class="fas fa-fw fas fa-user"></i>
<!-- Me -->
</div>
</div>
</div>
<div class="rightTableWrapper invisible">
<table class="right">
<thead>
<tr>
<td width="1%">#</td>
<td>name</td>
<td class="alignRight" width="1%">
wpm
<br />
<div class="sub">accuracy</div>
</td>
<td class="alignRight" width="1%">
raw
<br />
<div class="sub">consistency</div>
</td>
<td class="alignRight" width="120px">date</td>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
<dialog id="versionHistoryModal" class="modalWrapper hidden">
<div class="modal"></div>
</dialog>

View file

@ -221,16 +221,6 @@
}
}
}
#leaderboardsWrapper #leaderboards .leaderboardsTop {
grid-template-columns: 1fr 1fr;
grid-template-areas: "title title" "subtitle yesterday" "buttons buttons";
.buttons .timeRange {
grid-template-columns: 1fr 1fr 1fr;
.languageSelect {
grid-column: span 1;
}
}
}
.testActivity {
--box-size: 0.58em;
// .activity div,

View file

@ -221,37 +221,6 @@
}
}
}
#leaderboardsWrapper #leaderboards {
.tables {
grid-template-columns: 1fr;
height: 100%;
.titleAndTable .title {
font-size: 1rem;
}
.rightTableWrapper,
.leftTableWrapper {
height: calc(50vh - 9rem);
}
}
.leaderboardsTop {
.buttons .timeRange {
font-size: 0.75rem;
}
// grid-template-columns: 1fr 1fr;
// grid-template-areas: "title title" "subtitle yesterday" "buttons buttons";
.mainTitle {
font-size: 1.5rem;
}
// .buttons .timeRange {
// font-size: 0.75rem;
// grid-template-columns: 1fr 1fr 1fr;
// .languageSelect {
// grid-column: span 1;
// }
// }
}
}
#customTextModal {
.modal {
grid-template-areas:

View file

@ -33,16 +33,6 @@
font-size: 9rem;
}
}
#leaderboardsWrapper #leaderboards {
table thead tr td:nth-child(4),
table tbody tr td:nth-child(4),
table tfoot tr td:nth-child(4) {
display: none;
}
.mainTitle {
font-size: 2rem;
}
}
#customTextModal {
.modal {
.buttonsTop {

View file

@ -66,35 +66,7 @@ body {
// //1450px
// @media only screen and (max-width: 90.625rem) {
// #leaderboardsWrapper #leaderboards {
// .mainTitle {
// font-size: 2rem;
// }
// .title {
// font-size: 1rem;
// }
// .leaderboardsTop {
// grid-template-columns: auto 1fr max-content;
// .buttonGroup {
// grid-auto-flow: row;
// gap: 0.5rem;
// }
// }
// .tables table {
// .avatarNameBadge .badge .text {
// display: none;
// }
// tr td:first-child {
// padding-left: 0.25rem;
// }
// tr td:last-child {
// padding-right: 0.25rem;
// }
// td {
// padding: 0.25rem 0.5rem;
// }
// }
// }
//
// }
// //1330px
@ -116,13 +88,6 @@ body {
// //1250px
// @media only screen and (max-width: 78.125rem) {
// #leaderboardsWrapper #leaderboards {
// .tables table {
// tr td:nth-child(5) {
// display: none;
// }
// }
// }
// #customTextModal .modal {
// textarea {
// min-height: 426px;
@ -148,50 +113,6 @@ body {
// gap: 1rem;
// grid-template-rows: 1fr 1fr;
// }
// #leaderboardsWrapper {
// #leaderboards {
// .leaderboardsTop {
// flex-direction: column;
// align-items: baseline;
// grid-template-areas:
// "title title"
// "subtitle subtitle"
// "yesterday yesterday"
// "buttons buttons";
// grid-template-columns: 1fr;
// .buttons {
// margin-top: 0.5rem;
// }
// .buttonGroup {
// grid-auto-flow: column;
// }
// .showYesterdayButton {
// margin-left: 0;
// }
// }
// .tables {
// grid-template-columns: unset;
// table {
// .avatarNameBadge .badge .text {
// display: block;
// }
// tr td:last-child {
// width: 30%;
// }
// }
// }
// .tables .rightTableWrapper,
// .tables .leftTableWrapper {
// height: calc(50vh - 10rem);
// }
// .tables table {
// tr td:nth-child(5) {
// display: table-cell;
// width: auto;
// }
// }
// }
// }
// }
// //1000px
@ -313,11 +234,6 @@ body {
// grid-template-areas: "title title" "t15 t60";
// }
// }
// #leaderboards {
// .mainTitle {
// font-size: 1.5rem !important;
// }
// }
// #bannerCenter .banner .container {
// grid-template-columns: 1fr auto;
// .image {
@ -459,13 +375,6 @@ body {
// #quoteSearchModal .modal #quoteSearchControlsWrapper {
// grid-template-columns: 1fr;
// }
// #leaderboardsWrapper #leaderboards {
// .tables table {
// tr td:nth-child(5) {
// display: none;
// }
// }
// }
// .pageAbout {
// .triplegroup {
// grid-template-columns: 1fr;
@ -547,24 +456,6 @@ body {
// }
// }
// }
// #leaderboardsWrapper #leaderboards {
// width: 85vw;
// .tables .rightTableWrapper,
// .tables .leftTableWrapper {
// height: calc(50vh - 10rem);
// }
// .tables {
// grid-template-columns: unset;
// table {
// .avatarNameBadge .badge .text {
// display: block;
// }
// tr td:nth-child(4) {
// display: none;
// }
// }
// }
// }
// .profile {
// grid-template-columns: 1fr;
// grid-template-rows: auto auto auto;
@ -720,9 +611,6 @@ body {
// }
// }
// }
// #leaderboardsWrapper #leaderboards .leaderboardsTop .buttonGroup {
// grid-auto-flow: row;
// }
// .pageAccount {
// .group.history {
@ -972,32 +860,6 @@ body {
// }
// }
// }
// #leaderboardsWrapper #leaderboards {
// .tables .titleAndTable .titleAndButtons {
// grid-template-columns: unset;
// }
// .tables table {
// thead,
// tbody,
// tfoot {
// // font-size: 0.5rem;
// }
// tr td:first-child {
// padding-left: 0.25rem;
// }
// tr td:last-child {
// padding-right: 0.25rem;
// }
// td {
// padding: 0.25rem;
// }
// }
// .tables .rightTableWrapper,
// .tables .leftTableWrapper {
// height: calc(50vh - 9rem);
// }
// }
// .page404 .content {
// grid-template-columns: 1fr;
// }

View file

@ -864,12 +864,10 @@ async function handleTab(
$("#wordsInput").on("keydown", (event) => {
const pageTestActive: boolean = ActivePage.get() === "test";
const commandLineVisible = Misc.isPopupVisible("commandLineWrapper");
const leaderboardsVisible = Misc.isPopupVisible("leaderboardsWrapper");
const popupVisible: boolean = Misc.isAnyPopupVisible();
const allowTyping: boolean =
pageTestActive &&
!commandLineVisible &&
!leaderboardsVisible &&
!popupVisible &&
!TestUI.resultVisible &&
event.key !== "Enter" &&
@ -905,14 +903,12 @@ $(document).on("keydown", async (event) => {
const wordsFocused: boolean = $("#wordsInput").is(":focus");
const pageTestActive: boolean = ActivePage.get() === "test";
const commandLineVisible = Misc.isPopupVisible("commandLineWrapper");
const leaderboardsVisible = Misc.isPopupVisible("leaderboardsWrapper");
const popupVisible: boolean = Misc.isAnyPopupVisible();
const allowTyping: boolean =
pageTestActive &&
!commandLineVisible &&
!leaderboardsVisible &&
!popupVisible &&
!TestUI.resultVisible &&
(wordsFocused || event.key !== "Enter") &&

View file

@ -49,7 +49,10 @@ export function getSnapshot(): Snapshot | undefined {
return dbSnapshot;
}
export function setSnapshot(newSnapshot: Snapshot | undefined): void {
export function setSnapshot(
newSnapshot: Snapshot | undefined,
options?: { dispatchEvent?: boolean }
): void {
const originalBanned = dbSnapshot?.banned;
const originalVerified = dbSnapshot?.verified;
const lbOptOut = dbSnapshot?.lbOptOut;
@ -71,7 +74,9 @@ export function setSnapshot(newSnapshot: Snapshot | undefined): void {
dbSnapshot.lbOptOut = lbOptOut;
}
AuthEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: false } });
if (options?.dispatchEvent !== false) {
AuthEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: false } });
}
}
export async function initSnapshot(): Promise<Snapshot | false> {
@ -629,7 +634,7 @@ export async function getLocalPB<M extends Mode>(
);
}
export async function saveLocalPB<M extends Mode>(
function saveLocalPB<M extends Mode>(
mode: M,
mode2: Mode2<M>,
punctuation: boolean,
@ -641,7 +646,7 @@ export async function saveLocalPB<M extends Mode>(
acc: number,
raw: number,
consistency: number
): Promise<void> {
): void {
if (mode === "quote") return;
if (!dbSnapshot) return;
function cont(): void {
@ -922,38 +927,76 @@ export async function resetConfig(): Promise<void> {
}
}
export function saveLocalResult(result: SnapshotResult<Mode>): void {
export type SaveLocalResultData = {
xp?: number;
streak?: number;
result?: SnapshotResult<Mode>;
isPb?: boolean;
};
export function saveLocalResult(data: SaveLocalResultData): void {
const snapshot = getSnapshot();
if (!snapshot) return;
if (snapshot?.results !== undefined) {
snapshot.results.unshift(result);
if (data.result !== undefined) {
if (snapshot?.results !== undefined) {
snapshot.results.unshift(data.result);
}
if (snapshot.testActivity !== undefined) {
snapshot.testActivity.increment(new Date(data.result.timestamp));
}
if (snapshot.typingStats === undefined) {
snapshot.typingStats = {
timeTyping: 0,
startedTests: 0,
completedTests: 0,
};
setSnapshot(snapshot);
const time =
data.result.testDuration +
data.result.incompleteTestSeconds -
data.result.afkDuration;
snapshot.typingStats.timeTyping += time;
snapshot.typingStats.startedTests += data.result.restartCount + 1;
snapshot.typingStats.completedTests += 1;
}
if (data.isPb) {
saveLocalPB(
data.result.mode,
data.result.mode2,
data.result.punctuation,
data.result.numbers,
data.result.language,
data.result.difficulty,
data.result.lazyMode,
data.result.wpm,
data.result.acc,
data.result.rawWpm,
data.result.consistency
);
}
}
if (snapshot.testActivity !== undefined) {
snapshot.testActivity.increment(new Date(result.timestamp));
setSnapshot(snapshot);
}
}
export function updateLocalStats(started: number, time: number): void {
const snapshot = getSnapshot();
if (!snapshot) return;
if (snapshot.typingStats === undefined) {
snapshot.typingStats = {
timeTyping: 0,
startedTests: 0,
completedTests: 0,
};
if (data.xp !== undefined) {
if (snapshot.xp === undefined) {
snapshot.xp = 0;
}
snapshot.xp += data.xp;
}
snapshot.typingStats.timeTyping += time;
snapshot.typingStats.startedTests += started;
snapshot.typingStats.completedTests += 1;
if (data.streak !== undefined) {
snapshot.streak = data.streak;
setSnapshot(snapshot);
if (snapshot.streak > snapshot.maxStreak) {
snapshot.maxStreak = snapshot.streak;
}
}
setSnapshot(snapshot, {
dispatchEvent: false,
});
}
export function addXp(xp: number): void {
@ -964,7 +1007,9 @@ export function addXp(xp: number): void {
snapshot.xp = 0;
}
snapshot.xp += xp;
setSnapshot(snapshot);
setSnapshot(snapshot, {
dispatchEvent: false,
});
}
export function updateInboxUnreadSize(newSize: number): void {
@ -988,19 +1033,6 @@ export function addBadge(badge: Badge): void {
setSnapshot(snapshot);
}
export function setStreak(streak: number): void {
const snapshot = getSnapshot();
if (!snapshot) return;
snapshot.streak = streak;
if (snapshot.streak > snapshot.maxStreak) {
snapshot.maxStreak = snapshot.streak;
}
setSnapshot(snapshot);
}
export async function getTestActivityCalendar(
yearString: string
): Promise<TestActivityCalendar | undefined> {

View file

@ -1288,6 +1288,8 @@ async function saveResult(
);
$("#result .stats .tags .editTagsButton").removeClass("invisible");
const dataToSave: DB.SaveLocalResultData = {};
if (data.xp !== undefined) {
const snapxp = DB.getSnapshot()?.xp ?? 0;
@ -1296,11 +1298,11 @@ async function saveResult(
data.xp,
TestUI.resultVisible ? data.xpBreakdown : undefined
);
DB.addXp(data.xp);
dataToSave.xp = data.xp;
}
if (data.streak !== undefined) {
DB.setStreak(data.streak);
dataToSave.streak = data.streak;
}
if (data.insertedId !== undefined) {
@ -1314,13 +1316,7 @@ async function saveResult(
if (data.isPb !== undefined && data.isPb) {
result.isPb = true;
}
DB.saveLocalResult(result);
DB.updateLocalStats(
completedEvent.incompleteTests.length + 1,
completedEvent.testDuration +
completedEvent.incompleteTestSeconds -
completedEvent.afkDuration
);
dataToSave.result = result;
}
void AnalyticsController.log("testCompleted");
@ -1343,34 +1339,11 @@ async function saveResult(
}
Result.showCrown("normal");
await DB.saveLocalPB(
completedEvent.mode,
completedEvent.mode2,
completedEvent.punctuation,
completedEvent.numbers,
completedEvent.language,
completedEvent.difficulty,
completedEvent.lazyMode,
completedEvent.wpm,
completedEvent.acc,
completedEvent.rawWpm,
completedEvent.consistency
);
dataToSave.isPb = true;
} else {
Result.showErrorCrownIfNeeded();
}
// if (response.data.dailyLeaderboardRank) {
// Notifications.add(
// `New ${completedEvent.language} ${completedEvent.mode} ${completedEvent.mode2} rank: ` +
// Misc.getPositionString(response.data.dailyLeaderboardRank),
// 1,
// 10,
// "Daily Leaderboard",
// "list-ol"
// );
// }
if (data.dailyLeaderboardRank === undefined) {
$("#result .stats .dailyLeaderboard").addClass("hidden");
} else {
@ -1396,6 +1369,7 @@ async function saveResult(
if (isRetrying) {
Notifications.add("Result saved", 1, { important: true });
}
DB.saveLocalResult(dataToSave);
}
export function fail(reason: string): void {

View file

@ -27,15 +27,19 @@ export async function syncNotSignedInLastResult(uid: string): Promise<void> {
const result = structuredClone(
notSignedInLastResult
) as unknown as SnapshotResult<Mode>;
const dataToSave: DB.SaveLocalResultData = {
xp: response.body.data.xp,
streak: response.body.data.streak,
result,
isPb: response.body.data.isPb,
};
result._id = response.body.data.insertedId;
if (response.body.data.isPb) {
result.isPb = true;
}
DB.saveLocalResult(result);
DB.updateLocalStats(
1,
result.testDuration + result.incompleteTestSeconds - result.afkDuration
);
DB.saveLocalResult(dataToSave);
TestLogic.clearNotSignedInResult();
Notifications.add(
`Last test result saved ${response.body.data.isPb ? `(new pb!)` : ""}`,

View file

@ -26,15 +26,3 @@
.pageAbout .section p {
color: var(--sub-color);
}
#leaderboardsWrapper #leaderboards .title {
color: var(--sub-color);
}
#leaderboardsWrapper #leaderboards .tables table thead {
color: var(--sub-color);
}
#leaderboardsWrapper #leaderboards .tables table tbody {
color: var(--sub-color);
}