mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-01-09 00:45:32 +08:00
feat: Allow more results for premium users (fehmer, Miodec) (#4778)
* feat: Unlimited history for premium users UI * disable button while loading, show spinner * optional chaining * add isPremium to /users, remove check from frontend * add /results max batch size to configuration * result batching ui v1 * rename variables, update button based on state, update text * Return allowed results in /results if limit is partly overshooting the max limit * use br instead of margin * let result batches code handle button disabling * hide title * limit max width * hide section when below batch size * update limit color based on the % of limit used * bring back loaded bar * remove unused css * fix alignemtn * remove text for now * add result getting log * always hiding for non premium users for now * Add server configuration users.premium.enabled, throw error on /results if premium user exceeds regular limit and premium is globally disabled * Disable premium feature globally by default * cleanup open todos * Don't use premium user max limit on /results if premium feature is disabled on server * fix merge issue --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
643451b9cc
commit
1d4d7dab87
14 changed files with 442 additions and 54 deletions
|
|
@ -1,11 +1,11 @@
|
|||
import request from "supertest";
|
||||
import app from "../../../src/app";
|
||||
import _ from "lodash";
|
||||
import * as Configuration from "../../../src/init/configuration";
|
||||
import * as ResultDal from "../../../src/dal/result";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
import * as AuthUtils from "../../../src/utils/auth";
|
||||
import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier";
|
||||
import { messaging } from "firebase-admin";
|
||||
const uid = "123456";
|
||||
|
||||
const mockDecodedToken: DecodedIdToken = {
|
||||
|
|
@ -24,8 +24,9 @@ const configuration = Configuration.getCachedConfiguration();
|
|||
|
||||
describe("result controller test", () => {
|
||||
describe("getResults", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
resultMock.mockResolvedValue([]);
|
||||
await enablePremiumFeatures(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
resultMock.mockReset();
|
||||
|
|
@ -74,7 +75,7 @@ describe("result controller test", () => {
|
|||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ offset: 500, limit: 250 })
|
||||
.query({ limit: 250, offset: 500 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
|
@ -93,7 +94,7 @@ describe("result controller test", () => {
|
|||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ limit: 600, offset: 800 })
|
||||
.query({ limit: 100, offset: 1000 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(422)
|
||||
|
|
@ -116,7 +117,7 @@ describe("result controller test", () => {
|
|||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ offset: 600, limit: 800 })
|
||||
.query({ limit: 800, offset: 600 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
|
@ -129,6 +130,26 @@ describe("result controller test", () => {
|
|||
onOrAfterTimestamp: NaN,
|
||||
});
|
||||
});
|
||||
it("should get results if offset/limit is partly outside the max limit", async () => {
|
||||
//GIVEN
|
||||
jest.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ limit: 20, offset: 990 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
|
||||
expect(resultMock).toHaveBeenCalledWith(mockDecodedToken.uid, {
|
||||
limit: 10, //limit is reduced to stay within max limit
|
||||
offset: 990,
|
||||
onOrAfterTimestamp: NaN,
|
||||
});
|
||||
});
|
||||
it("should fail exceeding 1k limit", async () => {
|
||||
//GIVEN
|
||||
jest.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(false);
|
||||
|
|
@ -155,7 +176,7 @@ describe("result controller test", () => {
|
|||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ limit: 1000, offset: 24900 })
|
||||
.query({ limit: 1000, offset: 25000 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(422)
|
||||
|
|
@ -171,9 +192,74 @@ describe("result controller test", () => {
|
|||
|
||||
//THEN
|
||||
});
|
||||
it("should get results within regular limits for premium users even if premium is globally disabled", async () => {
|
||||
//GIVEN
|
||||
jest.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
|
||||
enablePremiumFeatures(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ limit: 100, offset: 900 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(resultMock).toHaveBeenCalledWith(mockDecodedToken.uid, {
|
||||
limit: 100,
|
||||
offset: 900,
|
||||
onOrAfterTimestamp: NaN,
|
||||
});
|
||||
});
|
||||
it("should fail exceeding max limit for premium user if premium is globally disabled", async () => {
|
||||
//GIVEN
|
||||
jest.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
|
||||
enablePremiumFeatures(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.query({ limit: 200, offset: 900 })
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(503)
|
||||
.expect(expectErrorMessage("Premium feature disabled."));
|
||||
|
||||
//THEN
|
||||
});
|
||||
it("should get results with regular limit as default for premium users if premium is globally disabled", async () => {
|
||||
//GIVEN
|
||||
jest.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
|
||||
enablePremiumFeatures(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/results")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(resultMock).toHaveBeenCalledWith(mockDecodedToken.uid, {
|
||||
limit: 1000, //the default limit for regular users
|
||||
offset: 0,
|
||||
onOrAfterTimestamp: NaN,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectErrorMessage(message: string): (res: request.Response) => void {
|
||||
return (res) => expect(res.body).toHaveProperty("message", message);
|
||||
}
|
||||
|
||||
async function enablePremiumFeatures(premium: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
users: { premium: { enabled: premium } },
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(Configuration, "getCachedConfiguration")
|
||||
.mockResolvedValue(mockConfig);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,24 +63,40 @@ export async function getResults(
|
|||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const isPremium = await UserDAL.checkIfUserIsPremium(uid);
|
||||
const premiumFeaturesEnabled = req.ctx.configuration.users.premium.enabled;
|
||||
const userHasPremium = await UserDAL.checkIfUserIsPremium(uid);
|
||||
|
||||
const maxLimit = isPremium
|
||||
? req.ctx.configuration.results.limits.premiumUser
|
||||
: req.ctx.configuration.results.limits.regularUser;
|
||||
const maxLimit =
|
||||
premiumFeaturesEnabled && userHasPremium
|
||||
? req.ctx.configuration.results.limits.premiumUser
|
||||
: req.ctx.configuration.results.limits.regularUser;
|
||||
|
||||
const onOrAfterTimestamp = parseInt(
|
||||
req.query.onOrAfterTimestamp as string,
|
||||
10
|
||||
);
|
||||
const limit = stringToNumberOrDefault(
|
||||
let limit = stringToNumberOrDefault(
|
||||
req.query.limit as string,
|
||||
Math.min(1000, maxLimit)
|
||||
Math.min(req.ctx.configuration.results.maxBatchSize, maxLimit)
|
||||
);
|
||||
const offset = stringToNumberOrDefault(req.query.offset as string, 0);
|
||||
|
||||
//check if premium features are disabled and current call exceeds the limit for regular users
|
||||
if (
|
||||
userHasPremium &&
|
||||
premiumFeaturesEnabled === false &&
|
||||
limit + offset > req.ctx.configuration.results.limits.regularUser
|
||||
) {
|
||||
throw new MonkeyError(503, "Premium feature disabled.");
|
||||
}
|
||||
|
||||
if (limit + offset > maxLimit) {
|
||||
throw new MonkeyError(422, `Max results limit of ${maxLimit} exceeded.`);
|
||||
if (offset < maxLimit) {
|
||||
//batch is partly in the allowed ranged. Set the limit to the max allowed and return partly results.
|
||||
limit = maxLimit - offset;
|
||||
} else {
|
||||
throw new MonkeyError(422, `Max results limit of ${maxLimit} exceeded.`);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await ResultDAL.getResults(uid, {
|
||||
|
|
@ -88,6 +104,16 @@ export async function getResults(
|
|||
limit,
|
||||
offset,
|
||||
});
|
||||
Logger.logToDb(
|
||||
"user_results_requested",
|
||||
{
|
||||
limit,
|
||||
offset,
|
||||
onOrAfterTimestamp,
|
||||
isPremium: userHasPremium,
|
||||
},
|
||||
uid
|
||||
);
|
||||
return new MonkeyResponse("Results retrieved", results);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -383,9 +383,12 @@ export async function getUser(
|
|||
UserDAL.flagForNameChange(uid);
|
||||
}
|
||||
|
||||
const isPremium = await UserDAL.checkIfUserIsPremium(uid);
|
||||
|
||||
const userData = {
|
||||
...getRelevantUserInfo(userInfo),
|
||||
inboxUnreadSize: inboxUnreadSize,
|
||||
isPremium,
|
||||
};
|
||||
|
||||
return new MonkeyResponse("User data retrieved", userData);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
regularUser: 1000,
|
||||
premiumUser: 10000,
|
||||
},
|
||||
maxBatchSize: 1000,
|
||||
},
|
||||
quotes: {
|
||||
reporting: {
|
||||
|
|
@ -69,6 +70,9 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
enabled: false,
|
||||
maxMail: 0,
|
||||
},
|
||||
premium: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
rateLimiting: {
|
||||
badAuthentication: {
|
||||
|
|
@ -189,6 +193,11 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema<MonkeyTypes.Configuration>
|
|||
},
|
||||
},
|
||||
},
|
||||
maxBatchSize: {
|
||||
type: "number",
|
||||
label: "results endpoint max batch size",
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
quotes: {
|
||||
|
|
@ -266,6 +275,16 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema<MonkeyTypes.Configuration>
|
|||
type: "object",
|
||||
label: "Users",
|
||||
fields: {
|
||||
premium: {
|
||||
type: "object",
|
||||
label: "Premium",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
},
|
||||
},
|
||||
signUp: {
|
||||
type: "boolean",
|
||||
label: "Sign Up Enabled",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface Configuration {
|
|||
regularUser: number;
|
||||
premiumUser: number;
|
||||
};
|
||||
maxBatchSize: number;
|
||||
};
|
||||
users: {
|
||||
signUp: boolean;
|
||||
|
|
@ -67,6 +68,9 @@ export interface Configuration {
|
|||
enabled: boolean;
|
||||
maxMail: number;
|
||||
};
|
||||
premium: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
admin: {
|
||||
endpointsEnabled: boolean;
|
||||
|
|
|
|||
2
backend/src/types/types.d.ts
vendored
2
backend/src/types/types.d.ts
vendored
|
|
@ -1,3 +1,5 @@
|
|||
type Configuration = import("../types/shared").Configuration;
|
||||
|
||||
type ObjectId = import("mongodb").ObjectId;
|
||||
|
||||
type ExpressRequest = import("express").Request;
|
||||
|
|
|
|||
|
|
@ -128,6 +128,74 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.resultBatches {
|
||||
display: grid;
|
||||
grid-template-areas: "bar button" "text text";
|
||||
grid-template-columns: 2fr 1fr;
|
||||
column-gap: 1rem;
|
||||
.title {
|
||||
grid-area: title;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
& > .text {
|
||||
grid-area: text;
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
grid-area: button;
|
||||
}
|
||||
.leftText,
|
||||
button,
|
||||
.rightText {
|
||||
align-self: center;
|
||||
}
|
||||
.bars {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 0.25rem 1rem;
|
||||
}
|
||||
.rightText {
|
||||
color: var(--sub-color);
|
||||
font-size: 0.8em;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.bar {
|
||||
height: 0.5rem;
|
||||
border-radius: var(--roundness);
|
||||
background: var(--sub-alt-color);
|
||||
position: relative;
|
||||
align-self: center;
|
||||
.fill {
|
||||
transition: width 0.125s;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--main-color);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
//not used for now
|
||||
.indicator {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
bottom: 0;
|
||||
.line {
|
||||
width: 0.1em;
|
||||
height: 1.5em;
|
||||
background: var(--sub-color);
|
||||
border-radius: var(--roundness);
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.text {
|
||||
font-size: 0.5em;
|
||||
color: var(--sub-color);
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.noDataError {
|
||||
margin: 20rem 0;
|
||||
text-align: center;
|
||||
|
|
@ -192,6 +260,7 @@
|
|||
|
||||
.title {
|
||||
color: var(--sub-color);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.val {
|
||||
|
|
@ -336,7 +405,6 @@
|
|||
display: grid;
|
||||
gap: 0.25rem;
|
||||
color: var(--sub-color);
|
||||
line-height: 1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
&.testDate .buttons,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ export default class Results {
|
|||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
async get(): Ape.EndpointResponse {
|
||||
return await this.httpClient.get(BASE_PATH);
|
||||
async get(offset?: number): Ape.EndpointResponse {
|
||||
return await this.httpClient.get(BASE_PATH, { searchQuery: { offset } });
|
||||
}
|
||||
|
||||
async save(
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export async function initSnapshot(): Promise<
|
|||
snap.streak = userData?.streak?.length ?? 0;
|
||||
snap.maxStreak = userData?.streak?.maxLength ?? 0;
|
||||
snap.filterPresets = userData.resultFilterPresets ?? [];
|
||||
snap.isPremium = userData?.isPremium;
|
||||
|
||||
const hourOffset = userData?.streak?.hourOffset;
|
||||
snap.streakHourOffset =
|
||||
|
|
@ -225,51 +226,67 @@ export async function initSnapshot(): Promise<
|
|||
}
|
||||
}
|
||||
|
||||
export async function getUserResults(): Promise<boolean> {
|
||||
export async function getUserResults(offset?: number): Promise<boolean> {
|
||||
const user = Auth?.currentUser;
|
||||
if (!user) return false;
|
||||
if (!dbSnapshot) return false;
|
||||
if (
|
||||
dbSnapshot.results !== undefined &&
|
||||
(offset === undefined || dbSnapshot.results.length > offset)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ConnectionState.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dbSnapshot.results !== undefined) {
|
||||
return true;
|
||||
} else {
|
||||
if (dbSnapshot.results === undefined) {
|
||||
LoadingPage.updateText("Downloading results...");
|
||||
LoadingPage.updateBar(90);
|
||||
|
||||
const response = await Ape.results.get();
|
||||
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error getting results: " + response.message, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const results = response.data as MonkeyTypes.Result<MonkeyTypes.Mode>[];
|
||||
results.forEach((result) => {
|
||||
if (result.bailedOut === undefined) result.bailedOut = false;
|
||||
if (result.blindMode === undefined) result.blindMode = false;
|
||||
if (result.lazyMode === undefined) result.lazyMode = false;
|
||||
if (result.difficulty === undefined) result.difficulty = "normal";
|
||||
if (result.funbox === undefined) result.funbox = "none";
|
||||
if (result.language === undefined || result.language === null) {
|
||||
result.language = "english";
|
||||
}
|
||||
if (result.numbers === undefined) result.numbers = false;
|
||||
if (result.punctuation === undefined) result.punctuation = false;
|
||||
if (result.quoteLength === undefined) result.quoteLength = -1;
|
||||
if (result.restartCount === undefined) result.restartCount = 0;
|
||||
if (result.incompleteTestSeconds === undefined) {
|
||||
result.incompleteTestSeconds = 0;
|
||||
}
|
||||
if (result.afkDuration === undefined) result.afkDuration = 0;
|
||||
if (result.tags === undefined) result.tags = [];
|
||||
});
|
||||
dbSnapshot.results = results?.sort((a, b) => b.timestamp - a.timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await Ape.results.get(offset);
|
||||
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error getting results: " + response.message, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const results = response.data as MonkeyTypes.Result<MonkeyTypes.Mode>[];
|
||||
results?.sort((a, b) => b.timestamp - a.timestamp);
|
||||
results.forEach((result) => {
|
||||
if (result.bailedOut === undefined) result.bailedOut = false;
|
||||
if (result.blindMode === undefined) result.blindMode = false;
|
||||
if (result.lazyMode === undefined) result.lazyMode = false;
|
||||
if (result.difficulty === undefined) result.difficulty = "normal";
|
||||
if (result.funbox === undefined) result.funbox = "none";
|
||||
if (result.language === undefined || result.language === null) {
|
||||
result.language = "english";
|
||||
}
|
||||
if (result.numbers === undefined) result.numbers = false;
|
||||
if (result.punctuation === undefined) result.punctuation = false;
|
||||
if (result.quoteLength === undefined) result.quoteLength = -1;
|
||||
if (result.restartCount === undefined) result.restartCount = 0;
|
||||
if (result.incompleteTestSeconds === undefined) {
|
||||
result.incompleteTestSeconds = 0;
|
||||
}
|
||||
if (result.afkDuration === undefined) result.afkDuration = 0;
|
||||
if (result.tags === undefined) result.tags = [];
|
||||
});
|
||||
|
||||
if (dbSnapshot.results !== undefined) {
|
||||
//merge
|
||||
const oldestTimestamp =
|
||||
dbSnapshot.results[dbSnapshot.results.length - 1].timestamp;
|
||||
const resultsWithoutDuplicates = results.filter(
|
||||
(it) => it.timestamp < oldestTimestamp
|
||||
);
|
||||
dbSnapshot.results.push(...resultsWithoutDuplicates);
|
||||
} else {
|
||||
dbSnapshot.results = results;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _getCustomThemeById(
|
||||
|
|
|
|||
122
frontend/src/ts/elements/result-batches.ts
Normal file
122
frontend/src/ts/elements/result-batches.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as DB from "../db";
|
||||
import * as ServerConfiguration from "../ape/server-configuration";
|
||||
import { blendTwoHexColors, mapRange } from "../utils/misc";
|
||||
import * as ThemeColors from "../elements/theme-colors";
|
||||
|
||||
export function hide(): void {
|
||||
$(".pageAccount .resultBatches").addClass("hidden");
|
||||
}
|
||||
|
||||
export function show(): void {
|
||||
$(".pageAccount .resultBatches").removeClass("hidden");
|
||||
}
|
||||
|
||||
export async function update(): Promise<void> {
|
||||
const results = DB.getSnapshot()?.results;
|
||||
|
||||
if (results === undefined) {
|
||||
console.error(
|
||||
"(Result batches) Results are missing but they should be available at the time of drawing the account page?"
|
||||
);
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
enableButton();
|
||||
|
||||
const completedTests = DB.getSnapshot()?.typingStats?.completedTests ?? 0;
|
||||
const percentageDownloaded = Math.round(
|
||||
(results.length / completedTests) * 100
|
||||
);
|
||||
const limits = ServerConfiguration.get()?.results.limits ?? {
|
||||
regularUser: 0,
|
||||
premiumUser: 0,
|
||||
};
|
||||
const currentLimit = DB.getSnapshot()?.isPremium
|
||||
? limits.premiumUser
|
||||
: limits.regularUser;
|
||||
const percentageLimit = Math.round((results?.length / currentLimit) * 100);
|
||||
|
||||
const barsWrapper = $(".pageAccount .resultBatches .bars");
|
||||
|
||||
const bars = {
|
||||
downloaded: {
|
||||
fill: barsWrapper.find(".downloaded .fill"),
|
||||
rightText: barsWrapper.find(".downloaded.rightText"),
|
||||
},
|
||||
limit: {
|
||||
fill: barsWrapper.find(".limit .fill"),
|
||||
rightText: barsWrapper.find(".limit.rightText"),
|
||||
},
|
||||
};
|
||||
|
||||
bars.downloaded.fill.css("width", Math.min(percentageDownloaded, 100) + "%");
|
||||
bars.downloaded.rightText.text(
|
||||
`${results?.length} / ${completedTests} (${percentageDownloaded}%)`
|
||||
);
|
||||
|
||||
const colors = await ThemeColors.getAll();
|
||||
|
||||
bars.limit.fill.css({
|
||||
width: Math.min(percentageLimit, 100) + "%",
|
||||
background: blendTwoHexColors(
|
||||
colors.sub,
|
||||
colors.error,
|
||||
mapRange(percentageLimit, 50, 100, 0, 1)
|
||||
),
|
||||
});
|
||||
bars.limit.rightText.text(
|
||||
`${results?.length} / ${currentLimit} (${percentageLimit}%)`
|
||||
);
|
||||
|
||||
const text = $(".pageAccount .resultBatches > .text");
|
||||
text.text("");
|
||||
|
||||
if (results.length >= completedTests) {
|
||||
disableButton();
|
||||
updateButtonText("all results loaded");
|
||||
}
|
||||
|
||||
if (results.length >= currentLimit) {
|
||||
disableButton();
|
||||
updateButtonText("limit reached");
|
||||
|
||||
// if (DB.getSnapshot()?.isPremium === false) {
|
||||
// text.html(
|
||||
// `<br>Want to load up to ${limits?.premiumUser} results and gain access to more perks? Join Monkeytype Premium.<br>`
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
export function disableButton(): void {
|
||||
$(".pageAccount .resultBatches button").prop("disabled", true);
|
||||
}
|
||||
|
||||
export function enableButton(): void {
|
||||
$(".pageAccount .resultBatches button").prop("disabled", false);
|
||||
}
|
||||
|
||||
export function updateButtonText(text: string): void {
|
||||
$(".pageAccount .resultBatches button").text(text);
|
||||
}
|
||||
|
||||
export function showOrHideIfNeeded(): void {
|
||||
//for now, just hide if not premium - will show this to everyone later
|
||||
const isPremium = DB.getSnapshot()?.isPremium ?? false;
|
||||
if (!isPremium) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const completed = DB.getSnapshot()?.typingStats?.completedTests ?? 0;
|
||||
const batchSize = ServerConfiguration.get()?.results.maxBatchSize ?? 0;
|
||||
|
||||
//no matter if premium or not, if the user is below the initial batch, hide the element
|
||||
if (completed <= batchSize) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
show();
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ import type { ScaleChartOptions, LinearScaleOptions } from "chart.js";
|
|||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import { Auth } from "../firebase";
|
||||
import * as Loader from "../elements/loader";
|
||||
import * as ResultBatches from "../elements/result-batches";
|
||||
|
||||
let filterDebug = false;
|
||||
//toggle filterdebug
|
||||
|
|
@ -222,6 +224,8 @@ async function fillContent(): Promise<void> {
|
|||
PbTables.update(snapshot.personalBests);
|
||||
Profile.update("account", snapshot);
|
||||
|
||||
ResultBatches.update();
|
||||
|
||||
chartData = [];
|
||||
accChartData = [];
|
||||
const wpmChartData: number[] = [];
|
||||
|
|
@ -1047,15 +1051,15 @@ async function fillContent(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
export async function downloadResults(): Promise<void> {
|
||||
if (DB.getSnapshot()?.results !== undefined) return;
|
||||
const results = await DB.getUserResults();
|
||||
export async function downloadResults(offset?: number): Promise<void> {
|
||||
const results = await DB.getUserResults(offset);
|
||||
if (results === false && !ConnectionState.get()) {
|
||||
Notifications.add("Could not get results - you are offline", -1, {
|
||||
duration: 5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
TodayTracker.addAllFromToday();
|
||||
if (results) {
|
||||
ResultFilters.updateActive();
|
||||
|
|
@ -1299,6 +1303,17 @@ $(".pageAccount .profile").on("click", ".details .copyLink", () => {
|
|||
);
|
||||
});
|
||||
|
||||
$(".pageAccount button.loadMoreResults").on("click", async () => {
|
||||
const offset = DB.getSnapshot()?.results?.length || 0;
|
||||
|
||||
Loader.show();
|
||||
ResultBatches.disableButton();
|
||||
|
||||
await downloadResults(offset);
|
||||
await fillContent();
|
||||
Loader.hide();
|
||||
});
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (ActivePage.get() === "account" && eventKey === "typingSpeedUnit") {
|
||||
update();
|
||||
|
|
@ -1327,6 +1342,7 @@ export const page = new Page(
|
|||
$(".pageAccount .content").addClass("hidden");
|
||||
$(".pageAccount .preloader").removeClass("hidden");
|
||||
}
|
||||
|
||||
await update();
|
||||
await Misc.sleep(0);
|
||||
updateChartColors();
|
||||
|
|
@ -1336,6 +1352,8 @@ export const page = new Page(
|
|||
`<p class="accountVerificatinNotice" style="text-align:center">Your account is not verified. <a class="sendVerificationEmail">Send the verification email again</a>.`
|
||||
);
|
||||
}
|
||||
|
||||
ResultBatches.showOrHideIfNeeded();
|
||||
},
|
||||
async () => {
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as Skeleton from "../popups/skeleton";
|
|||
|
||||
export function updateBar(percentage: number, fast = false): void {
|
||||
const speed = fast ? 100 : 1000;
|
||||
$(".pageLoading .fill, .pageAccount .fill")
|
||||
$(".pageLoading .fill, .pageAccount .preloader .fill")
|
||||
.stop(true, fast)
|
||||
.animate(
|
||||
{
|
||||
|
|
|
|||
1
frontend/src/ts/types/types.d.ts
vendored
1
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -607,6 +607,7 @@ declare namespace MonkeyTypes {
|
|||
maxStreak: number;
|
||||
streakHourOffset?: number;
|
||||
lbOptOut?: boolean;
|
||||
isPremium?: boolean;
|
||||
}
|
||||
|
||||
interface UserDetails {
|
||||
|
|
|
|||
|
|
@ -304,6 +304,28 @@
|
|||
<div id="ad-account-1-small"></div>
|
||||
</div>
|
||||
|
||||
<div class="group resultBatches hidden">
|
||||
<!-- <div class="title">result history</div> -->
|
||||
<div class="bars">
|
||||
<div class="leftText downloaded">Result history:</div>
|
||||
<div class="bar downloaded">
|
||||
<div class="fill"></div>
|
||||
</div>
|
||||
<div class="rightText downloaded">-</div>
|
||||
<div class="leftText limit">Result limit:</div>
|
||||
<div class="bar limit">
|
||||
<div class="fill"></div>
|
||||
<!-- <div class="indicator">
|
||||
<div class="text">completed tests</div>
|
||||
<div class="line"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="rightText limit">-</div>
|
||||
</div>
|
||||
<div class="text"></div>
|
||||
<button class="loadMoreResults">load more</button>
|
||||
</div>
|
||||
|
||||
<div class="group presetFilterButtons hidden">
|
||||
<div class="buttonsAndTitle">
|
||||
<div class="title">filter presets</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue