Merge remote-tracking branch 'upstream/master' into store_custom_themes

This commit is contained in:
Rizwan Mustafa 2022-03-07 12:58:12 +05:00
commit a47f108a33
20 changed files with 644 additions and 67 deletions

View file

@ -32,7 +32,7 @@ class ApeKeysController {
if (currentNumberOfApeKeys >= maxKeysPerUser) {
throw new MonkeyError(
500,
400,
"Maximum number of ApeKeys have been generated"
);
}

View file

@ -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;

View file

@ -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(

View file

@ -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;

View file

@ -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;

View file

@ -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;

View 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";
},
});

View file

@ -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,
};

View file

@ -27,6 +27,7 @@ declare namespace MonkeyTypes {
}
interface DecodedToken {
type?: "Bearer" | "ApeKey";
uid?: string;
email?: string;
}

View file

@ -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 });
}

View file

@ -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;
};

View file

@ -271,6 +271,7 @@ export async function setup(challengeName: string): Promise<boolean> {
ConfigEvent.subscribe((eventKey) => {
if (
[
"difficulty",
"numbers",
"punctuation",
"mode",

View file

@ -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",
() => {

View 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);
}
});

View file

@ -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",

View file

@ -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,
};

View file

@ -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;

View file

@ -10,6 +10,10 @@ textarea {
font-family: var(--font);
}
textarea {
resize: vertical;
}
input[type="range"] {
-webkit-appearance: none;
padding: 0;

View file

@ -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);

View file

@ -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">