mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-10 22:22:21 +08:00
Merge remote-tracking branch 'upstream/master' into store_custom_themes
This commit is contained in:
commit
a47f108a33
20 changed files with 644 additions and 67 deletions
|
|
@ -32,7 +32,7 @@ class ApeKeysController {
|
|||
|
||||
if (currentNumberOfApeKeys >= maxKeysPerUser) {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
400,
|
||||
"Maximum number of ApeKeys have been generated"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,13 +211,17 @@ class UserController {
|
|||
return new MonkeyResponse("Leaderboard memory updated");
|
||||
}
|
||||
|
||||
static async getCustomThemes(req, _res): Promise<MonkeyResponse> {
|
||||
static async getCustomThemes(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const customThemes = await UsersDAO.getThemes(uid);
|
||||
return new MonkeyResponse("Custom themes retrieved", customThemes);
|
||||
}
|
||||
|
||||
static async addCustomTheme(req, _res): Promise<MonkeyResponse> {
|
||||
static async addCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const customTheme = req.body;
|
||||
|
||||
|
|
@ -227,20 +231,34 @@ class UserController {
|
|||
});
|
||||
}
|
||||
|
||||
static async removeCustomTheme(req, _res): Promise<MonkeyResponse> {
|
||||
static async removeCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { themeId } = req.body;
|
||||
await UsersDAO.removeTheme(uid, themeId);
|
||||
return new MonkeyResponse("Custom theme removed");
|
||||
}
|
||||
|
||||
static async editCustomTheme(req, _res): Promise<MonkeyResponse> {
|
||||
static async editCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { themeId, theme } = req.body;
|
||||
|
||||
await UsersDAO.editTheme(uid, themeId, theme);
|
||||
return new MonkeyResponse("Custom theme updated");
|
||||
}
|
||||
|
||||
static async getPersonalBests(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { mode, mode2 } = req.params;
|
||||
|
||||
const data = (await UsersDAO.getPersonalBests(uid, mode, mode2)) ?? null;
|
||||
return new MonkeyResponse("Personal bests retrieved", data);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserController;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
validateConfiguration,
|
||||
validateRequest,
|
||||
} from "../../middlewares/api-utils";
|
||||
import SUPPORTED_QUOTE_LANGUAGES from "../../constants/quote-languages";
|
||||
|
||||
const quotesRouter = Router();
|
||||
|
||||
|
|
@ -119,10 +118,7 @@ quotesRouter.post(
|
|||
validateRequest({
|
||||
body: {
|
||||
quoteId: joi.string().required(),
|
||||
quoteLanguage: joi
|
||||
.string()
|
||||
.valid(...SUPPORTED_QUOTE_LANGUAGES)
|
||||
.required(),
|
||||
quoteLanguage: joi.string().required(),
|
||||
reason: joi
|
||||
.string()
|
||||
.valid(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Router } from "express";
|
|||
import UserController from "../controllers/user";
|
||||
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import ApeRateLimit from "../../middlewares/ape-rate-limit";
|
||||
import { isUsernameValid } from "../../utils/validation";
|
||||
|
||||
const router = Router();
|
||||
|
|
@ -286,4 +287,26 @@ router.post(
|
|||
asyncHandler(UserController.unlinkDiscord)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/personalBests/:mode/",
|
||||
RateLimit.userGet,
|
||||
authenticateRequest({
|
||||
isPublic: false,
|
||||
acceptApeKeys: true,
|
||||
}),
|
||||
ApeRateLimit,
|
||||
asyncHandler(UserController.getPersonalBests)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/personalBests/:mode/:mode2",
|
||||
RateLimit.userGet,
|
||||
authenticateRequest({
|
||||
isPublic: false,
|
||||
acceptApeKeys: true,
|
||||
}),
|
||||
ApeRateLimit,
|
||||
asyncHandler(UserController.getPersonalBests)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
const SUPPORTED_QUOTE_LANGUAGES = [
|
||||
"albanian",
|
||||
"arabic",
|
||||
"code_c++",
|
||||
"code_c",
|
||||
"code_java",
|
||||
"code_javascript",
|
||||
"code_python",
|
||||
"code_rust",
|
||||
"czech",
|
||||
"danish",
|
||||
"dutch",
|
||||
"english",
|
||||
"filipino",
|
||||
"french",
|
||||
"german",
|
||||
"hindi",
|
||||
"icelandic",
|
||||
"indonesian",
|
||||
"irish",
|
||||
"italian",
|
||||
"lithuanian",
|
||||
"malagasy",
|
||||
"polish",
|
||||
"portuguese",
|
||||
"russian",
|
||||
"serbian",
|
||||
"slovak",
|
||||
"spanish",
|
||||
"swedish",
|
||||
"thai",
|
||||
"toki_pona",
|
||||
"turkish",
|
||||
"vietnamese",
|
||||
];
|
||||
|
||||
export default SUPPORTED_QUOTE_LANGUAGES;
|
||||
|
|
@ -380,6 +380,15 @@ class UsersDAO {
|
|||
static async setApeKeys(uid, apeKeys) {
|
||||
await db.collection("users").updateOne({ uid }, { $set: { apeKeys } });
|
||||
}
|
||||
|
||||
static async getPersonalBests(uid, mode, mode2) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (mode2) {
|
||||
return user?.personalBests?.[mode]?.[mode2];
|
||||
} else {
|
||||
return user?.personalBests?.[mode];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UsersDAO;
|
||||
|
|
|
|||
31
backend/middlewares/ape-rate-limit.ts
Normal file
31
backend/middlewares/ape-rate-limit.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Response, NextFunction } from "express";
|
||||
import rateLimit, { Options } from "express-rate-limit";
|
||||
import MonkeyError from "../utils/error";
|
||||
|
||||
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1;
|
||||
|
||||
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
|
||||
return req?.ctx?.decodedToken?.uid;
|
||||
};
|
||||
|
||||
const customHandler = (
|
||||
_req: MonkeyTypes.Request,
|
||||
_res: Response,
|
||||
_next: NextFunction,
|
||||
_options: Options
|
||||
): void => {
|
||||
throw new MonkeyError(429, "Too many attempts, please try again later.");
|
||||
};
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
|
||||
export default rateLimit({
|
||||
windowMs: ONE_MINUTE,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKey,
|
||||
handler: customHandler,
|
||||
skip: (req: MonkeyTypes.Request, _res) => {
|
||||
const decodedToken = req?.ctx?.decodedToken;
|
||||
return decodedToken?.type !== "ApeKey";
|
||||
},
|
||||
});
|
||||
|
|
@ -75,6 +75,7 @@ function authenticateWithBody(
|
|||
}
|
||||
|
||||
return {
|
||||
type: "Bearer",
|
||||
uid,
|
||||
};
|
||||
}
|
||||
|
|
@ -110,6 +111,7 @@ async function authenticateWithBearerToken(
|
|||
const decodedToken = await verifyIdToken(token);
|
||||
|
||||
return {
|
||||
type: "Bearer",
|
||||
uid: decodedToken.uid,
|
||||
email: decodedToken.email,
|
||||
};
|
||||
|
|
@ -169,6 +171,7 @@ async function authenticateWithApeKey(
|
|||
await ApeKeysDAO.updateLastUsedOn(keyOwner, keyId);
|
||||
|
||||
return {
|
||||
type: "ApeKey",
|
||||
uid,
|
||||
email: keyOwner.email,
|
||||
};
|
||||
|
|
|
|||
1
backend/types/types.d.ts
vendored
1
backend/types/types.d.ts
vendored
|
|
@ -27,6 +27,7 @@ declare namespace MonkeyTypes {
|
|||
}
|
||||
|
||||
interface DecodedToken {
|
||||
type?: "Bearer" | "ApeKey";
|
||||
uid?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,9 @@ export default function getApeKeysEndpoints(
|
|||
|
||||
async function update(
|
||||
apeKeyId: string,
|
||||
name: string,
|
||||
enabled: boolean
|
||||
updates: { name?: string; enabled?: boolean }
|
||||
): Ape.EndpointData {
|
||||
const payload = { name, enabled };
|
||||
const payload = { ...updates };
|
||||
return await apeClient.patch(`${BASE_PATH}/${apeKeyId}`, { payload });
|
||||
}
|
||||
|
||||
|
|
|
|||
3
frontend/src/scripts/ape/types/ape.d.ts
vendored
3
frontend/src/scripts/ape/types/ape.d.ts
vendored
|
|
@ -145,8 +145,7 @@ declare namespace Ape {
|
|||
generate: (name: string, enabled: boolean) => EndpointData;
|
||||
update: (
|
||||
apeKeyId: string,
|
||||
name: string,
|
||||
enabled: boolean
|
||||
updates: { name?: string; enabled?: boolean }
|
||||
) => EndpointData;
|
||||
delete: (apeKeyId: string) => EndpointData;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (
|
||||
[
|
||||
"difficulty",
|
||||
"numbers",
|
||||
"punctuation",
|
||||
"mode",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import * as Notifications from "../elements/notifications";
|
|||
import * as ImportExportSettingsPopup from "../popups/import-export-settings-popup";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import * as ApeKeysPopup from "../popups/ape-keys-popup";
|
||||
import Page from "./page";
|
||||
|
||||
type SettingsGroups = {
|
||||
|
|
@ -926,10 +927,37 @@ $("#exportSettingsButton").click(() => {
|
|||
);
|
||||
});
|
||||
|
||||
$(".pageSettings .sectionGroupTitle").click((e) => {
|
||||
$("#shareCustomThemeButton").on("click", () => {
|
||||
const share: string[] = [];
|
||||
$.each(
|
||||
$(".pageSettings .section.customTheme [type='color']"),
|
||||
(_, element) => {
|
||||
share.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
|
||||
const url =
|
||||
"https://monkeytype.com?" +
|
||||
Misc.objectToQueryString({ customTheme: share });
|
||||
|
||||
navigator.clipboard.writeText(url).then(
|
||||
function () {
|
||||
Notifications.add("URL Copied to clipboard", 0);
|
||||
},
|
||||
function () {
|
||||
// CustomThemePopup.show(url);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".pageSettings .sectionGroupTitle").on("click", (e) => {
|
||||
toggleSettingsGroup($(e.currentTarget).attr("group") as string);
|
||||
});
|
||||
|
||||
$(".pageSettings .section.apeKeys #showApeKeysPopup").on("click", () => {
|
||||
ApeKeysPopup.show();
|
||||
});
|
||||
|
||||
$(".pageSettings .section.customBackgroundSize .inputAndButton .save").on(
|
||||
"click",
|
||||
() => {
|
||||
|
|
|
|||
137
frontend/src/scripts/popups/ape-keys-popup.ts
Normal file
137
frontend/src/scripts/popups/ape-keys-popup.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import Ape from "../ape";
|
||||
import * as Loader from "../elements/loader";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
|
||||
let apeKeys: MonkeyTypes.ApeKeys = {};
|
||||
|
||||
async function getData(): Promise<void> {
|
||||
Loader.show();
|
||||
const response = await Ape.apeKeys.get();
|
||||
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error getting ape keys: " + response.message, -1);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
apeKeys = response.data as MonkeyTypes.ApeKeys;
|
||||
Loader.hide();
|
||||
}
|
||||
|
||||
function refreshList(): void {
|
||||
const data = apeKeys;
|
||||
if (!data) return;
|
||||
const table = $("#apeKeysPopupWrapper table tbody");
|
||||
table.empty();
|
||||
const apeKeyIds = Object.keys(data);
|
||||
if (apeKeyIds.length === 0) {
|
||||
table.append(
|
||||
"<tr><td colspan='6' style='text-align: center;'>No keys found</td></tr>"
|
||||
);
|
||||
return;
|
||||
}
|
||||
apeKeyIds.forEach((apeKeyId) => {
|
||||
const key = data[apeKeyId] as MonkeyTypes.ApeKey;
|
||||
table.append(`
|
||||
<tr keyId="${apeKeyId}">
|
||||
<td>
|
||||
<div class="icon-button">
|
||||
${
|
||||
key.enabled
|
||||
? `<i class="fas fa-check-square"></i>`
|
||||
: `<i class="far fa-fw fa-square"></i>`
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td>${key.name}</td>
|
||||
<td>${moment(key.createdOn).format("DD MMM YYYY HH:mm")}</td>
|
||||
<td>${moment(key.modifiedOn).format("DD MMM YYYY HH:mm")}</td>
|
||||
<td>${
|
||||
key.lastUsedOn === -1
|
||||
? "-"
|
||||
: moment(key.lastUsedOn).format("DD MMM YYYY HH:mm")
|
||||
}</td>
|
||||
<td>
|
||||
<div class="keyButtons">
|
||||
<div class="button edit">
|
||||
<i class="fas fa-fw fa-pen"></i>
|
||||
</div>
|
||||
<div class="button delete">
|
||||
<i class="fas fa-fw fa-trash-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (!$("#apeKeysPopupWrapper").hasClass("hidden")) {
|
||||
$("#apeKeysPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
100,
|
||||
() => {
|
||||
$("#apeKeysPopupWrapper").addClass("hidden");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//show the popup
|
||||
export async function show(): Promise<void> {
|
||||
if ($("#apeKeysPopupWrapper").hasClass("hidden")) {
|
||||
await getData();
|
||||
refreshList();
|
||||
$("#apeKeysPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
100,
|
||||
() => {
|
||||
$("#apeKeysPopup textarea").focus().select();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$("#apeKeysPopupWrapper").on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "apeKeysPopupWrapper") {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
$("#apeKeysPopup .generateApeKey").on("click", () => {
|
||||
hide();
|
||||
});
|
||||
|
||||
$(document).on("click", "#apeKeysPopup table .keyButtons .button", () => {
|
||||
hide();
|
||||
});
|
||||
|
||||
$(document).on("click", "#apeKeysPopup table .icon-button", async (e) => {
|
||||
const keyId = $(e.target).closest("tr").attr("keyId") as string;
|
||||
const key = apeKeys?.[keyId];
|
||||
if (!key || !apeKeys) return;
|
||||
Loader.show();
|
||||
const response = await Ape.apeKeys.update(keyId, { enabled: !key.enabled });
|
||||
Loader.hide();
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add("Failed to update key: " + response.message, -1);
|
||||
}
|
||||
apeKeys[keyId].enabled = !key.enabled;
|
||||
refreshList();
|
||||
if (key.enabled) {
|
||||
Notifications.add("Key active", 1);
|
||||
} else {
|
||||
Notifications.add("Key inactive", 1);
|
||||
}
|
||||
});
|
||||
|
|
@ -6,14 +6,18 @@ import * as UpdateConfig from "../config";
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as Settings from "../pages/settings";
|
||||
import * as ApeKeysPopup from "../popups/ape-keys-popup";
|
||||
|
||||
type Input = {
|
||||
placeholder: string;
|
||||
type?: string;
|
||||
initVal: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
let activePopup: SimplePopup | null = null;
|
||||
|
||||
export const list: { [key: string]: SimplePopup } = {};
|
||||
class SimplePopup {
|
||||
parameters: string[];
|
||||
|
|
@ -26,7 +30,9 @@ class SimplePopup {
|
|||
text: string;
|
||||
buttonText: string;
|
||||
execFn: (thisPopup: SimplePopup, ...params: string[]) => void | Promise<void>;
|
||||
beforeInitFn: (thisPopup: SimplePopup) => void;
|
||||
beforeShowFn: (thisPopup: SimplePopup) => void;
|
||||
canClose: boolean;
|
||||
constructor(
|
||||
id: string,
|
||||
type: string,
|
||||
|
|
@ -38,6 +44,7 @@ class SimplePopup {
|
|||
thisPopup: SimplePopup,
|
||||
...params: string[]
|
||||
) => void | Promise<void>,
|
||||
beforeInitFn: (thisPopup: SimplePopup) => void,
|
||||
beforeShowFn: (thisPopup: SimplePopup) => void
|
||||
) {
|
||||
this.parameters = [];
|
||||
|
|
@ -51,7 +58,9 @@ class SimplePopup {
|
|||
this.wrapper = $("#simplePopupWrapper");
|
||||
this.element = $("#simplePopup");
|
||||
this.buttonText = buttonText;
|
||||
this.beforeInitFn = (thisPopup): void => beforeInitFn(thisPopup);
|
||||
this.beforeShowFn = (thisPopup): void => beforeShowFn(thisPopup);
|
||||
this.canClose = true;
|
||||
}
|
||||
reset(): void {
|
||||
this.element.html(`
|
||||
|
|
@ -78,6 +87,12 @@ class SimplePopup {
|
|||
el.find(".button").text(this.buttonText);
|
||||
}
|
||||
|
||||
if (this.text === "") {
|
||||
el.find(".text").addClass("hidden");
|
||||
} else {
|
||||
el.find(".text").removeClass("hidden");
|
||||
}
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +105,7 @@ class SimplePopup {
|
|||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
val="${input.initVal}"
|
||||
value="${input.initVal}"
|
||||
placeholder="${input.placeholder}"
|
||||
class="${input.hidden ? "hidden" : ""}"
|
||||
${input.hidden ? "" : "required"}
|
||||
|
|
@ -101,24 +116,38 @@ class SimplePopup {
|
|||
} else if (this.type === "text") {
|
||||
this.inputs.forEach((input) => {
|
||||
if (input.type) {
|
||||
el.find(".inputs").append(`
|
||||
if (input.type === "textarea") {
|
||||
el.find(".inputs").append(`
|
||||
<textarea
|
||||
placeholder="${input.placeholder}"
|
||||
class="${input.hidden ? "hidden" : ""}"
|
||||
${input.hidden ? "" : "required"}
|
||||
${input.disabled ? "disabled" : ""}
|
||||
autocomplete="off"
|
||||
>${input.initVal}</textarea>
|
||||
`);
|
||||
} else {
|
||||
el.find(".inputs").append(`
|
||||
<input
|
||||
type="${input.type}"
|
||||
val="${input.initVal}"
|
||||
placeholder="${input.placeholder}"
|
||||
class="${input.hidden ? "hidden" : ""}"
|
||||
${input.hidden ? "" : "required"}
|
||||
autocomplete="off"
|
||||
type="${input.type}"
|
||||
value="${input.initVal}"
|
||||
placeholder="${input.placeholder}"
|
||||
class="${input.hidden ? "hidden" : ""}"
|
||||
${input.hidden ? "" : "required"}
|
||||
${input.disabled ? "disabled" : ""}
|
||||
autocomplete="off"
|
||||
>
|
||||
`);
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
el.find(".inputs").append(`
|
||||
<input
|
||||
type="text"
|
||||
val="${input.initVal}"
|
||||
value="${input.initVal}"
|
||||
placeholder="${input.placeholder}"
|
||||
class="${input.hidden ? "hidden" : ""}"
|
||||
${input.hidden ? "" : "required"}
|
||||
${input.disabled ? "disabled" : ""}
|
||||
autocomplete="off"
|
||||
>
|
||||
`);
|
||||
|
|
@ -132,6 +161,7 @@ class SimplePopup {
|
|||
}
|
||||
|
||||
exec(): void {
|
||||
if (!this.canClose) return;
|
||||
const vals: string[] = [];
|
||||
$.each($("#simplePopup input"), (_, el) => {
|
||||
vals.push($(el).val() as string);
|
||||
|
|
@ -141,9 +171,11 @@ class SimplePopup {
|
|||
}
|
||||
|
||||
show(parameters: string[] = []): void {
|
||||
activePopup = this;
|
||||
this.parameters = parameters;
|
||||
this.beforeShowFn(this);
|
||||
this.beforeInitFn(this);
|
||||
this.init();
|
||||
this.beforeShowFn(this);
|
||||
this.wrapper
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
|
|
@ -154,6 +186,8 @@ class SimplePopup {
|
|||
}
|
||||
|
||||
hide(): void {
|
||||
if (!this.canClose) return;
|
||||
activePopup = null;
|
||||
this.wrapper
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
|
|
@ -165,6 +199,7 @@ class SimplePopup {
|
|||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (activePopup) return activePopup.hide();
|
||||
$("#simplePopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
|
|
@ -176,6 +211,7 @@ export function hide(): void {
|
|||
|
||||
$("#simplePopupWrapper").mousedown((e) => {
|
||||
if ($(e.target).attr("id") === "simplePopupWrapper") {
|
||||
if (activePopup) return activePopup.hide();
|
||||
$("#simplePopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
|
|
@ -266,6 +302,9 @@ list["updateEmail"] = new SimplePopup(
|
|||
thisPopup.buttonText = "";
|
||||
thisPopup.text = "Password authentication is not enabled";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -338,6 +377,9 @@ list["updateName"] = new SimplePopup(
|
|||
thisPopup.inputs[0].hidden = true;
|
||||
thisPopup.buttonText = "Reauthenticate to update";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -400,6 +442,9 @@ list["updatePassword"] = new SimplePopup(
|
|||
thisPopup.buttonText = "";
|
||||
thisPopup.text = "Password authentication is not enabled";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -449,6 +494,9 @@ list["addPasswordAuth"] = new SimplePopup(
|
|||
},
|
||||
() => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -526,6 +574,9 @@ list["deleteAccount"] = new SimplePopup(
|
|||
thisPopup.inputs = [];
|
||||
thisPopup.buttonText = "Reauthenticate to delete";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -569,6 +620,9 @@ list["clearTagPb"] = new SimplePopup(
|
|||
},
|
||||
(thisPopup) => {
|
||||
thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`;
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -585,6 +639,9 @@ list["applyCustomFont"] = new SimplePopup(
|
|||
},
|
||||
() => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -643,6 +700,9 @@ list["resetPersonalBests"] = new SimplePopup(
|
|||
thisPopup.inputs = [];
|
||||
thisPopup.buttonText = "Reauthenticate to reset";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -661,6 +721,9 @@ list["resetSettings"] = new SimplePopup(
|
|||
},
|
||||
() => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -689,6 +752,136 @@ list["unlinkDiscord"] = new SimplePopup(
|
|||
},
|
||||
() => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
list["generateApeKey"] = new SimplePopup(
|
||||
"generateApeKey",
|
||||
"text",
|
||||
"Generate new key",
|
||||
[
|
||||
{
|
||||
placeholder: "Name",
|
||||
initVal: "",
|
||||
},
|
||||
],
|
||||
"",
|
||||
"Generate",
|
||||
async (_thisPopup, name) => {
|
||||
Loader.show();
|
||||
const response = await Ape.apeKeys.generate(name, false);
|
||||
Loader.hide();
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add(
|
||||
"Failed to generate key: " + response.message,
|
||||
-1
|
||||
);
|
||||
} else {
|
||||
const data = response.data;
|
||||
list["viewApeKey"].show([data.apeKey]);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
list["viewApeKey"] = new SimplePopup(
|
||||
"viewApeKey",
|
||||
"text",
|
||||
"Ape Key",
|
||||
[
|
||||
{
|
||||
type: "textarea",
|
||||
disabled: true,
|
||||
placeholder: "Key",
|
||||
initVal: "",
|
||||
},
|
||||
],
|
||||
"This is your new Ape Key. Please keep it safe. You will only see it once!",
|
||||
"Close",
|
||||
(_thisPopup) => {
|
||||
ApeKeysPopup.show();
|
||||
},
|
||||
(_thisPopup) => {
|
||||
_thisPopup.inputs[0].initVal = _thisPopup.parameters[0];
|
||||
},
|
||||
(_thisPopup) => {
|
||||
_thisPopup.canClose = false;
|
||||
$("#simplePopup textarea").css("height", "110px");
|
||||
$("#simplePopup .button").addClass("hidden");
|
||||
setTimeout(() => {
|
||||
_thisPopup.canClose = true;
|
||||
$("#simplePopup .button").removeClass("hidden");
|
||||
}, 3000);
|
||||
}
|
||||
);
|
||||
|
||||
list["deleteApeKey"] = new SimplePopup(
|
||||
"deleteApeKey",
|
||||
"text",
|
||||
"Delete Ape Key",
|
||||
[],
|
||||
"Are you sure?",
|
||||
"Delete",
|
||||
async (_thisPopup) => {
|
||||
Loader.show();
|
||||
const response = await Ape.apeKeys.delete(_thisPopup.parameters[0]);
|
||||
Loader.hide();
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add("Failed to delete key: " + response.message, -1);
|
||||
}
|
||||
|
||||
Notifications.add("Key deleted", 1);
|
||||
ApeKeysPopup.show();
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
list["editApeKey"] = new SimplePopup(
|
||||
"editApeKey",
|
||||
"text",
|
||||
"Edit Ape Key",
|
||||
[
|
||||
{
|
||||
placeholder: "Name",
|
||||
initVal: "",
|
||||
},
|
||||
],
|
||||
"",
|
||||
"Edit",
|
||||
async (_thisPopup, input) => {
|
||||
Loader.show();
|
||||
const response = await Ape.apeKeys.update(_thisPopup.parameters[0], {
|
||||
name: input,
|
||||
});
|
||||
Loader.hide();
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Notifications.add("Failed to update key: " + response.message, -1);
|
||||
}
|
||||
|
||||
Notifications.add("Key updated", 1);
|
||||
ApeKeysPopup.show();
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -726,6 +919,20 @@ $(".pageSettings #deleteAccount").on("click", () => {
|
|||
list["deleteAccount"].show();
|
||||
});
|
||||
|
||||
$("#apeKeysPopup .generateApeKey").on("click", () => {
|
||||
list["generateApeKey"].show();
|
||||
});
|
||||
|
||||
$(document).on("click", "#apeKeysPopup table tbody tr .button.delete", (e) => {
|
||||
const keyId = $(e.target).closest("tr").attr("keyId") as string;
|
||||
list["deleteApeKey"].show([keyId]);
|
||||
});
|
||||
|
||||
$(document).on("click", "#apeKeysPopup table tbody tr .button.edit", (e) => {
|
||||
const keyId = $(e.target).closest("tr").attr("keyId") as string;
|
||||
list["editApeKey"].show([keyId]);
|
||||
});
|
||||
|
||||
$(document).on(
|
||||
"click",
|
||||
".pageSettings .section.fontFamily .button.custom",
|
||||
|
|
|
|||
|
|
@ -397,8 +397,8 @@ function countChars(): CharCount {
|
|||
|
||||
export function calculateStats(): Stats {
|
||||
let testSeconds = calculateTestSeconds();
|
||||
console.log((end2 - start2) / 1000);
|
||||
console.log(testSeconds);
|
||||
// console.log((end2 - start2) / 1000);
|
||||
// console.log(testSeconds);
|
||||
if (Config.mode != "custom") {
|
||||
testSeconds = Misc.roundTo2(testSeconds);
|
||||
}
|
||||
|
|
@ -428,7 +428,7 @@ export function calculateStats(): Stats {
|
|||
chars.spaces +
|
||||
chars.incorrectChars +
|
||||
chars.extraChars,
|
||||
time: testSeconds,
|
||||
time: Misc.roundTo2(testSeconds),
|
||||
spaces: chars.spaces,
|
||||
correctSpaces: chars.correctSpaces,
|
||||
};
|
||||
|
|
|
|||
12
frontend/src/scripts/types/types.d.ts
vendored
12
frontend/src/scripts/types/types.d.ts
vendored
|
|
@ -250,6 +250,18 @@ declare namespace MonkeyTypes {
|
|||
hash?: string;
|
||||
}
|
||||
|
||||
type ApeKey = {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
lastUsedOn: number;
|
||||
};
|
||||
|
||||
interface ApeKeys {
|
||||
[key: string]: ApeKey;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
theme: string;
|
||||
themeLight: string;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ textarea {
|
|||
font-family: var(--font);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -522,6 +522,112 @@
|
|||
}
|
||||
}
|
||||
|
||||
#apeKeysPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
width: 1000px;
|
||||
max-width: calc(100vw - 4rem);
|
||||
// height: 100%;
|
||||
// max-height: 40rem;
|
||||
min-height: 18rem;
|
||||
overflow-y: scroll;
|
||||
grid-template-rows: max-content auto;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
|
||||
.top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
.button {
|
||||
padding: 0.4rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.keyButtons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1rem;
|
||||
.button {
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
tr td:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tr.me {
|
||||
td {
|
||||
color: var(--main-color);
|
||||
// font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
thead {
|
||||
color: var(--sub-color);
|
||||
font-size: 0.75rem;
|
||||
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-color);
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
color: var(--text-color);
|
||||
|
||||
tr:nth-child(odd) td {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
tfoot {
|
||||
td {
|
||||
padding: 1rem 0.5rem;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
bottom: -5px;
|
||||
background: var(--bg-color);
|
||||
color: var(--main-color);
|
||||
z-index: 4;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td:first-child {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
td:last-child {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#quoteApprovePopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
|
|
|
|||
|
|
@ -551,6 +551,30 @@
|
|||
<div id="submitQuoteButton" class="button">Submit</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="apeKeysPopupWrapper" class="hidden popupWrapper">
|
||||
<div id="apeKeysPopup" mode="">
|
||||
<div class="top">
|
||||
<div class="title">Ape Keys</div>
|
||||
<div class="button generateApeKey">
|
||||
<i class="fas fa-plus"></i>
|
||||
Generate new key
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td width="1px">active</td>
|
||||
<td>name</td>
|
||||
<td>created on</td>
|
||||
<td>modified on</td>
|
||||
<td>last used on</td>
|
||||
<td width="1px"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="quoteReportPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="quoteReportPopup" mode="">
|
||||
<div class="title">Report a Quote</div>
|
||||
|
|
@ -4148,6 +4172,22 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="section apeKeys needsAccount">
|
||||
<h1>ape keys</h1>
|
||||
<div class="text">
|
||||
Generate Ape Keys to access certain API endpoints.
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
class="button"
|
||||
id="showApeKeysPopup"
|
||||
tabindex="0"
|
||||
onclick="this.blur();"
|
||||
>
|
||||
open
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="section resetSettings">
|
||||
<h1>reset settings</h1>
|
||||
<div class="text">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue