mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-25 07:17:23 +08:00
Merge branch 'master' into offline
This commit is contained in:
commit
2920722315
19 changed files with 252 additions and 36 deletions
|
|
@ -25,7 +25,16 @@ export async function createNewUser(
|
|||
const { name, captcha } = req.body;
|
||||
const { email, uid } = req.ctx.decodedToken;
|
||||
|
||||
await verifyCaptcha(captcha);
|
||||
try {
|
||||
await verifyCaptcha(captcha);
|
||||
} catch (e) {
|
||||
try {
|
||||
await admin.auth().deleteUser(uid);
|
||||
} catch (e) {
|
||||
// user might be deleted on the frontend
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) {
|
||||
throw new MonkeyError(400, "Invalid domain");
|
||||
|
|
|
|||
|
|
@ -81,7 +81,9 @@ const CONFIG_SCHEMA = joi.object({
|
|||
soundVolume: joi.string().valid("0.1", "0.5", "1.0"),
|
||||
startGraphsAtZero: joi.boolean(),
|
||||
showOutOfFocusWarning: joi.boolean(),
|
||||
paceCaret: joi.string().valid("off", "average", "pb", "last", "custom"),
|
||||
paceCaret: joi
|
||||
.string()
|
||||
.valid("off", "average", "pb", "last", "daily", "custom"),
|
||||
paceCaretCustomSpeed: joi.number().min(0),
|
||||
repeatedPace: joi.boolean(),
|
||||
pageWidth: joi.string().valid("100", "125", "150", "200", "max"),
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ async function authenticateWithBearerToken(
|
|||
options: RequestAuthenticationOptions
|
||||
): Promise<MonkeyTypes.DecodedToken> {
|
||||
try {
|
||||
const decodedToken = await verifyIdToken(token);
|
||||
const decodedToken = await verifyIdToken(token, options.requireFreshToken);
|
||||
|
||||
if (options.requireFreshToken) {
|
||||
const now = Date.now();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,14 @@ const tokenCache = new LRUCache<string, DecodedIdToken>({
|
|||
|
||||
const TOKEN_CACHE_BUFFER = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
export async function verifyIdToken(idToken: string): Promise<DecodedIdToken> {
|
||||
export async function verifyIdToken(
|
||||
idToken: string,
|
||||
noCache = false
|
||||
): Promise<DecodedIdToken> {
|
||||
if (noCache) {
|
||||
return await admin.auth().verifyIdToken(idToken, true);
|
||||
}
|
||||
|
||||
setTokenCacheLength(tokenCache.size);
|
||||
setTokenCacheSize(tokenCache.calculatedSize ?? 0);
|
||||
|
||||
|
|
|
|||
36
frontend/package-lock.json
generated
36
frontend/package-lock.json
generated
|
|
@ -9,8 +9,8 @@
|
|||
"version": "1.14.3",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"axios": "0.21.4",
|
||||
"canvas-confetti": "1.5.1",
|
||||
"chart.js": "3.7.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-annotation": "1.4.0",
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"throttle-debounce": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/canvas-confetti": "1.4.3",
|
||||
"@types/chartjs-plugin-trendline": "1.0.1",
|
||||
"@types/damerau-levenshtein": "1.0.0",
|
||||
"@types/grecaptcha": "3.0.4",
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
"@types/jquery": "3.5.14",
|
||||
"@types/object-hash": "2.2.1",
|
||||
"@types/select2": "4.0.55",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"buffer": "6.0.3",
|
||||
"circular-dependency-plugin": "5.2.2",
|
||||
|
|
@ -951,6 +953,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/canvas-confetti": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz",
|
||||
"integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/chartjs-plugin-trendline": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz",
|
||||
|
|
@ -1158,7 +1166,8 @@
|
|||
"node_modules/@types/throttle-debounce": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
|
||||
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ=="
|
||||
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tinycolor2": {
|
||||
"version": "1.4.3",
|
||||
|
|
@ -2548,6 +2557,15 @@
|
|||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
},
|
||||
"node_modules/canvas-confetti": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz",
|
||||
"integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg==",
|
||||
"funding": {
|
||||
"type": "donate",
|
||||
"url": "https://www.paypal.me/kirilvatev"
|
||||
}
|
||||
},
|
||||
"node_modules/centra": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz",
|
||||
|
|
@ -15157,6 +15175,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/canvas-confetti": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz",
|
||||
"integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/chartjs-plugin-trendline": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz",
|
||||
|
|
@ -15364,7 +15388,8 @@
|
|||
"@types/throttle-debounce": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
|
||||
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ=="
|
||||
"integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/tinycolor2": {
|
||||
"version": "1.4.3",
|
||||
|
|
@ -16453,6 +16478,11 @@
|
|||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz",
|
||||
"integrity": "sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA=="
|
||||
},
|
||||
"canvas-confetti": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz",
|
||||
"integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg=="
|
||||
},
|
||||
"centra": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"npm": "8.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/canvas-confetti": "1.4.3",
|
||||
"@types/chartjs-plugin-trendline": "1.0.1",
|
||||
"@types/damerau-levenshtein": "1.0.0",
|
||||
"@types/grecaptcha": "3.0.4",
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"axios": "0.21.4",
|
||||
"canvas-confetti": "1.5.1",
|
||||
"chart.js": "3.7.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-annotation": "1.4.0",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ const subgroup: MonkeyTypes.CommandsSubgroup = {
|
|||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "setPaceCaretDaily",
|
||||
display: "daily",
|
||||
configValue: "daily",
|
||||
exec: (): void => {
|
||||
UpdateConfig.setPaceCaret("daily");
|
||||
TestLogic.restart();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "setPaceCaretCustom",
|
||||
display: "custom...",
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ export function setPaceCaret(
|
|||
): boolean {
|
||||
if (
|
||||
!isConfigValueValid("pace caret", val, [
|
||||
["custom", "off", "average", "pb", "last"],
|
||||
["custom", "off", "average", "pb", "last", "daily"],
|
||||
])
|
||||
) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -608,30 +608,6 @@ async function signUp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Force user to use a capital letter, number, special character when setting up an account and changing password
|
||||
if (password.length < 8) {
|
||||
Notifications.add("Password must be at least 8 characters", 0, 3);
|
||||
LoginPage.hidePreloader();
|
||||
LoginPage.enableInputs();
|
||||
LoginPage.updateSignupButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCapital = password.match(/[A-Z]/);
|
||||
const hasNumber = password.match(/[\d]/);
|
||||
const hasSpecial = password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/);
|
||||
if (!hasCapital || !hasNumber || !hasSpecial) {
|
||||
Notifications.add(
|
||||
"Password must contain at least one capital letter, number, and special character",
|
||||
0,
|
||||
3
|
||||
);
|
||||
LoginPage.hidePreloader();
|
||||
LoginPage.enableInputs();
|
||||
LoginPage.updateSignupButton();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== passwordVerify) {
|
||||
Notifications.add("Passwords do not match", 0, 3);
|
||||
LoginPage.hidePreloader();
|
||||
|
|
@ -640,6 +616,22 @@ async function signUp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Force user to use a capital letter, number, special character when setting up an account and changing password
|
||||
if (
|
||||
window.location.hostname !== "localhost" &&
|
||||
!Misc.isPasswordStrong(password)
|
||||
) {
|
||||
Notifications.add(
|
||||
"Password must contain at least one capital letter, number, a special character and at least 8 characters long",
|
||||
0,
|
||||
4
|
||||
);
|
||||
LoginPage.hidePreloader();
|
||||
LoginPage.enableInputs();
|
||||
LoginPage.updateSignupButton();
|
||||
return;
|
||||
}
|
||||
|
||||
authListener();
|
||||
|
||||
let createdAuthUser;
|
||||
|
|
@ -688,8 +680,16 @@ async function signUp(): Promise<void> {
|
|||
} catch (e) {
|
||||
//make sure to do clean up here
|
||||
if (createdAuthUser) {
|
||||
await Ape.users.delete();
|
||||
await createdAuthUser.user.delete();
|
||||
try {
|
||||
await Ape.users.delete();
|
||||
} catch (e) {
|
||||
// account might already be deleted
|
||||
}
|
||||
try {
|
||||
await createdAuthUser.user.delete();
|
||||
} catch (e) {
|
||||
// account might already be deleted
|
||||
}
|
||||
}
|
||||
console.log(e);
|
||||
const message = Misc.createErrorMessage(e, "Failed to create account");
|
||||
|
|
|
|||
|
|
@ -428,6 +428,65 @@ export async function getUserAverage10<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function getUserDailyBest<M extends MonkeyTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
lazyMode: boolean
|
||||
): Promise<number> {
|
||||
const snapshot = getSnapshot();
|
||||
|
||||
if (!snapshot) return 0;
|
||||
|
||||
function cont(): number {
|
||||
const activeTagIds: string[] = [];
|
||||
snapshot.tags?.forEach((tag) => {
|
||||
if (tag.active === true) {
|
||||
activeTagIds.push(tag._id);
|
||||
}
|
||||
});
|
||||
|
||||
let bestWpm = 0;
|
||||
|
||||
if (snapshot.results !== undefined) {
|
||||
for (const result of snapshot.results) {
|
||||
if (
|
||||
result.mode === mode &&
|
||||
result.punctuation === punctuation &&
|
||||
result.language === language &&
|
||||
result.difficulty === difficulty &&
|
||||
(result.lazyMode === lazyMode ||
|
||||
(result.lazyMode === undefined && lazyMode === false)) &&
|
||||
(activeTagIds.length === 0 ||
|
||||
activeTagIds.some((tagId) => result.tags.includes(tagId)))
|
||||
) {
|
||||
if (result.timestamp < Date.now() - 86400000) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continue if the mode2 doesn't match and it's not a quote
|
||||
if (result.mode2 !== mode2 && mode !== "quote") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.wpm > bestWpm) {
|
||||
bestWpm = result.wpm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestWpm;
|
||||
}
|
||||
|
||||
const retval: number =
|
||||
snapshot === null || (await getUserResults()) === null ? 0 : cont();
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
export async function getLocalPB<M extends MonkeyTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ export async function update(): Promise<void> {
|
|||
? "pb"
|
||||
: Config.paceCaret === "last"
|
||||
? "last"
|
||||
: Config.paceCaret === "daily"
|
||||
? "daily"
|
||||
: "custom"
|
||||
} pace${speed}</div>`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
unlink,
|
||||
updatePassword,
|
||||
} from "firebase/auth";
|
||||
import { isPasswordStrong } from "../utils/misc";
|
||||
|
||||
interface Input {
|
||||
placeholder?: string;
|
||||
|
|
@ -524,6 +525,17 @@ list["updatePassword"] = new SimplePopup(
|
|||
Notifications.add("New passwords don't match", 0);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.location.hostname !== "localhost" &&
|
||||
!isPasswordStrong(newPass)
|
||||
) {
|
||||
Notifications.add(
|
||||
"New password must contain at least one capital letter, number, a special character and at least 8 characters long",
|
||||
0,
|
||||
4
|
||||
);
|
||||
return;
|
||||
}
|
||||
Loader.show();
|
||||
await reauthenticateWithCredential(user, credential);
|
||||
await updatePassword(user, newPass);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@ export async function init(): Promise<void> {
|
|||
Config.lazyMode
|
||||
);
|
||||
wpm = Math.round(wpm);
|
||||
} else if (Config.paceCaret === "daily") {
|
||||
wpm = await DB.getUserDailyBest(
|
||||
Config.mode,
|
||||
mode2,
|
||||
Config.punctuation,
|
||||
Config.language,
|
||||
Config.difficulty,
|
||||
Config.lazyMode
|
||||
);
|
||||
wpm = Math.round(wpm);
|
||||
} else if (Config.paceCaret === "custom") {
|
||||
wpm = Config.paceCaretCustomSpeed;
|
||||
} else if (Config.paceCaret === "last" || TestState.isPaceRepeat == true) {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ import * as AdController from "../controllers/ad-controller";
|
|||
import * as TestConfig from "./test-config";
|
||||
import { Chart } from "chart.js";
|
||||
import { Auth } from "../firebase";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports -- need to ignore because eslint doesnt know what import type is
|
||||
import type { PluginChartOptions, ScaleChartOptions } from "chart.js";
|
||||
import type { AnnotationOptions } from "chartjs-plugin-annotation";
|
||||
import Ape from "../ape";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
let result: MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
let maxChartVal: number;
|
||||
|
|
@ -343,6 +345,38 @@ export function showCrown(): void {
|
|||
PbCrown.show();
|
||||
}
|
||||
|
||||
export function showConfetti(): void {
|
||||
if (SlowTimer.get()) return;
|
||||
const style = getComputedStyle(document.body);
|
||||
const colors = [
|
||||
style.getPropertyValue("--main-color"),
|
||||
style.getPropertyValue("--text-color"),
|
||||
style.getPropertyValue("--sub-color"),
|
||||
];
|
||||
const duration = Date.now() + 125;
|
||||
|
||||
(function f(): void {
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 60,
|
||||
spread: 75,
|
||||
origin: { x: 0 },
|
||||
colors: colors,
|
||||
});
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 120,
|
||||
spread: 75,
|
||||
origin: { x: 1 },
|
||||
colors: colors,
|
||||
});
|
||||
|
||||
if (Date.now() < duration) {
|
||||
requestAnimationFrame(f);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export function hideCrown(): void {
|
||||
PbCrown.hide();
|
||||
$("#result .stats .wpm .crown").attr("aria-label", "");
|
||||
|
|
|
|||
|
|
@ -1738,6 +1738,11 @@ async function saveResult(
|
|||
|
||||
if (response?.data?.isPb) {
|
||||
//new pb
|
||||
if (
|
||||
DB.getSnapshot()?.personalBests?.[Config.mode]?.[completedEvent.mode2]
|
||||
) {
|
||||
Result.showConfetti();
|
||||
}
|
||||
Result.showCrown();
|
||||
Result.updateCrown();
|
||||
DB.saveLocalPB(
|
||||
|
|
|
|||
2
frontend/src/ts/types/types.d.ts
vendored
2
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -108,7 +108,7 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type SoundVolume = "0.1" | "0.5" | "1.0";
|
||||
|
||||
type PaceCaret = "off" | "average" | "pb" | "last" | "custom";
|
||||
type PaceCaret = "off" | "average" | "pb" | "last" | "custom" | "daily";
|
||||
|
||||
type PageWidth = "100" | "125" | "150" | "200" | "max";
|
||||
|
||||
|
|
|
|||
|
|
@ -1235,3 +1235,11 @@ export function abbreviateNumber(num: number): string {
|
|||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function isPasswordStrong(password: string): boolean {
|
||||
const hasCapital = !!password.match(/[A-Z]/);
|
||||
const hasNumber = !!password.match(/[\d]/);
|
||||
const hasSpecial = !!password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/);
|
||||
const isLong = password.length >= 8;
|
||||
return hasCapital && hasNumber && hasSpecial && isLong;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,16 @@
|
|||
<!-- Initialize Firebase -->
|
||||
<script src="/__/firebase/init.js?useEmulator=true'"></script>
|
||||
<script defer>
|
||||
function isPasswordStrong(password) {
|
||||
const hasCapital = !!password.match(/[A-Z]/);
|
||||
const hasNumber = !!password.match(/[\d]/);
|
||||
const hasSpecial = !!password.match(
|
||||
/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/
|
||||
);
|
||||
const isLong = password.length >= 8;
|
||||
return hasCapital && hasNumber && hasSpecial && isLong;
|
||||
}
|
||||
|
||||
function handleVerifyEmail(actionCode, continueUrl) {
|
||||
firebase
|
||||
.auth()
|
||||
|
|
@ -232,6 +242,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (!isPasswordStrong(newPassword)) {
|
||||
alert(
|
||||
"Password must be at least 8 characters long and contain at least one capital letter, one number and one special character."
|
||||
);
|
||||
showResetPassword();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the new password.
|
||||
firebase
|
||||
.auth()
|
||||
|
|
|
|||
|
|
@ -1035,7 +1035,8 @@
|
|||
<h1>pace caret</h1>
|
||||
<div class="text">
|
||||
Displays a second caret that moves at constant speed. The 'average'
|
||||
option averages the speed of last 10 results.
|
||||
option averages the speed of last 10 results. The 'daily' option takes
|
||||
the highest speed of the last 24 hours.
|
||||
</div>
|
||||
<div>
|
||||
<div class="inputAndButton">
|
||||
|
|
@ -1085,6 +1086,14 @@
|
|||
>
|
||||
last
|
||||
</div>
|
||||
<div
|
||||
class="button"
|
||||
paceCaret="daily"
|
||||
tabindex="0"
|
||||
onclick="this.blur();"
|
||||
>
|
||||
daily
|
||||
</div>
|
||||
<div
|
||||
class="button"
|
||||
paceCaret="custom"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue