From 484ab1bd5f1f7c598bd615da316567e5ca99174b Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 18 Dec 2025 19:20:15 +0100 Subject: [PATCH 1/2] chore: remove jquery from toggleSettingsGroup --- frontend/src/styles/settings.scss | 1 + frontend/src/ts/pages/settings.ts | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss index f37ed4b68c31..2dcc02cabd25 100644 --- a/frontend/src/styles/settings.scss +++ b/frontend/src/styles/settings.scss @@ -50,6 +50,7 @@ .settingsGroup { display: grid; gap: 2rem; + overflow: hidden; &.quickNav { justify-content: center; .links { diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index c6707aaf0373..885550626361 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -43,7 +43,7 @@ import * as CustomBackgroundPicker from "../elements/settings/custom-background- import * as CustomFontPicker from "../elements/settings/custom-font-picker"; import * as AuthEvent from "../observables/auth-event"; import * as FpsLimitSection from "../elements/settings/fps-limit-section"; -import { qsr } from "../utils/dom"; +import { qs, qsr } from "../utils/dom"; let settingsInitialized = false; @@ -759,13 +759,28 @@ function toggleSettingsGroup(groupName: string): void { //The highlight is repeated/broken when toggling the group handleHighlightSection(undefined); - const groupEl = $(`.pageSettings .settingsGroup.${groupName}`); - groupEl.stop(true, true).slideToggle(250).toggleClass("slideup"); - if (groupEl.hasClass("slideup")) { + const groupEl = qs(`.pageSettings .settingsGroup.${groupName}`); + if (!groupEl?.hasClass("slideup")) { + groupEl?.animate({ + height: 0, + duration: 250, + onComplete: () => { + groupEl?.hide(); + }, + }); + groupEl?.addClass("slideup"); $(`.pageSettings .sectionGroupTitle[group=${groupName}]`).addClass( "rotateIcon", ); } else { + groupEl?.show(); + groupEl?.setStyle({ height: "" }); + const height = groupEl.getOffsetHeight(); + groupEl?.animate({ + height: [0, height], + duration: 250, + }); + groupEl?.removeClass("slideup"); $(`.pageSettings .sectionGroupTitle[group=${groupName}]`).removeClass( "rotateIcon", ); From 8a2a3e4d233c9be3f6611ee982cf18ae501b5509 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 18 Dec 2025 19:27:24 +0100 Subject: [PATCH 2/2] impr: add copy details to notification history (@fehmer) (#7262) Add details to notifications. If details are available show share icon on hover in the notification history. On click the full content is copied to the clipboard. With this is easier for an user to share the full details of an error on github or discord. image image ```json { "title": "Error", "message": "Failed to save config", "details": { "status": 422, "validationErrors": [ "Unrecognized key(s) in object: 'invalid'" ] } } ``` --------- Co-authored-by: Jack --- frontend/src/styles/popups.scss | 6 +- frontend/src/ts/auth.ts | 9 +- frontend/src/ts/db.ts | 26 ++-- .../account-settings/ape-key-table.ts | 13 +- .../account-settings/blocked-user-table.ts | 5 +- frontend/src/ts/elements/alerts.ts | 119 ++++++++++++++++-- frontend/src/ts/elements/notifications.ts | 23 +++- frontend/src/ts/modals/dev-options.ts | 1 + frontend/src/ts/modals/edit-preset.ts | 9 +- frontend/src/ts/modals/edit-profile.ts | 2 +- frontend/src/ts/modals/edit-result-tags.ts | 5 +- frontend/src/ts/modals/edit-tag.ts | 10 +- frontend/src/ts/modals/quote-approve.ts | 8 +- frontend/src/ts/modals/quote-rate.ts | 10 +- frontend/src/ts/modals/quote-report.ts | 2 +- frontend/src/ts/modals/quote-submit.ts | 2 +- frontend/src/ts/modals/simple-modals.ts | 39 +++--- frontend/src/ts/modals/streak-hour-offset.ts | 5 +- frontend/src/ts/modals/user-report.ts | 2 +- .../src/ts/observables/notification-event.ts | 13 +- frontend/src/ts/pages/account.ts | 2 +- frontend/src/ts/test/test-logic.ts | 2 +- frontend/src/ts/utils/results.ts | 5 +- frontend/src/ts/utils/url-handler.ts | 2 +- packages/contracts/src/util/api.ts | 16 +++ 25 files changed, 227 insertions(+), 109 deletions(-) diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss index 82501c7c2455..644d865acad6 100644 --- a/frontend/src/styles/popups.scss +++ b/frontend/src/styles/popups.scss @@ -1863,8 +1863,7 @@ body.darkMode { } } .notificationHistory .list .item { - grid-template-areas: "indicator title" "indicator body"; - grid-template-columns: 0.25rem calc(100% - 0.25rem); + grid-template-areas: "indicator title buttons" "indicator body buttons"; .title { font-size: 0.75rem; color: var(--sub-color); @@ -1872,6 +1871,9 @@ body.darkMode { .body { opacity: 1; } + .highlight { + color: var(--main-color) !important; + } } .accountAlerts { .title { diff --git a/frontend/src/ts/auth.ts b/frontend/src/ts/auth.ts index 8e33f5822057..da87b66a29ee 100644 --- a/frontend/src/ts/auth.ts +++ b/frontend/src/ts/auth.ts @@ -45,14 +45,11 @@ async function sendVerificationEmail(): Promise { Loader.show(); qs(".sendVerificationEmail")?.disable(); - const result = await Ape.users.verificationEmail(); + const response = await Ape.users.verificationEmail(); qs(".sendVerificationEmail")?.enable(); - if (result.status !== 200) { + if (response.status !== 200) { Loader.hide(); - Notifications.add( - "Failed to request verification email: " + result.body.message, - -1, - ); + Notifications.add("Failed to request verification email", -1, { response }); } else { Loader.hide(); Notifications.add("Verification email sent", 1); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index a8e4301520f1..0089fc07458e 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -299,7 +299,7 @@ export async function getUserResults(offset?: number): Promise { const response = await Ape.results.get({ query: { offset } }); if (response.status !== 200) { - Notifications.add("Error getting results: " + response.body.message, -1); + Notifications.add("Error getting results", -1, { response }); return false; } @@ -357,10 +357,7 @@ export async function addCustomTheme( const response = await Ape.users.addCustomTheme({ body: { ...theme } }); if (response.status !== 200) { - Notifications.add( - "Error adding custom theme: " + response.body.message, - -1, - ); + Notifications.add("Error adding custom theme", -1, { response }); return false; } @@ -400,10 +397,7 @@ export async function editCustomTheme( body: { themeId, theme: newTheme }, }); if (response.status !== 200) { - Notifications.add( - "Error editing custom theme: " + response.body.message, - -1, - ); + Notifications.add("Error editing custom theme", -1, { response }); return false; } @@ -427,10 +421,7 @@ export async function deleteCustomTheme(themeId: string): Promise { const response = await Ape.users.deleteCustomTheme({ body: { themeId } }); if (response.status !== 200) { - Notifications.add( - "Error deleting custom theme: " + response.body.message, - -1, - ); + Notifications.add("Error deleting custom theme", -1, { response }); return false; } @@ -923,7 +914,7 @@ export async function saveConfig(config: Partial): Promise { if (isAuthenticated()) { const response = await Ape.configs.save({ body: config }); if (response.status !== 200) { - Notifications.add("Failed to save config: " + response.body.message, -1); + Notifications.add("Failed to save config", -1, { response }); } } } @@ -932,7 +923,7 @@ export async function resetConfig(): Promise { if (isAuthenticated()) { const response = await Ape.configs.delete(); if (response.status !== 200) { - Notifications.add("Failed to reset config: " + response.body.message, -1); + Notifications.add("Failed to reset config", -1, { response }); } } } @@ -1055,10 +1046,7 @@ export async function getTestActivityCalendar( Loader.show(); const response = await Ape.users.getTestActivity(); if (response.status !== 200) { - Notifications.add( - "Error getting test activities: " + response.body.message, - -1, - ); + Notifications.add("Error getting test activities", -1, { response }); Loader.hide(); return undefined; } diff --git a/frontend/src/ts/elements/account-settings/ape-key-table.ts b/frontend/src/ts/elements/account-settings/ape-key-table.ts index 83c27771c161..0d845b7c0969 100644 --- a/frontend/src/ts/elements/account-settings/ape-key-table.ts +++ b/frontend/src/ts/elements/account-settings/ape-key-table.ts @@ -27,7 +27,8 @@ const editApeKey = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to update key: " + response.body.message, + message: "Failed to update key", + notificationOptions: { response }, }; } return { @@ -53,7 +54,8 @@ const deleteApeKeyModal = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to delete key: " + response.body.message, + message: "Failed to delete key", + notificationOptions: { response }, }; } @@ -128,7 +130,8 @@ const generateApeKey = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to generate key: " + response.body.message, + message: "Failed to generate key", + notificationOptions: { response }, }; } @@ -174,7 +177,7 @@ async function getData(): Promise { void update(); return false; } - Notifications.add("Error getting ape keys: " + response.body.message, -1); + Notifications.add("Error getting ape keys", -1, { response }); return false; } @@ -261,7 +264,7 @@ async function toggleActiveKey(keyId: string): Promise { }); Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to update key: " + response.body.message, -1); + Notifications.add("Failed to update key", -1, { response }); return; } key.enabled = !key.enabled; diff --git a/frontend/src/ts/elements/account-settings/blocked-user-table.ts b/frontend/src/ts/elements/account-settings/blocked-user-table.ts index 57839e54a6a6..9bb28e169905 100644 --- a/frontend/src/ts/elements/account-settings/blocked-user-table.ts +++ b/frontend/src/ts/elements/account-settings/blocked-user-table.ts @@ -24,10 +24,7 @@ async function getData(): Promise { if (response.status !== 200) { blockedUsers = []; - Notifications.add( - "Error getting blocked users: " + response.body.message, - -1, - ); + Notifications.add("Error getting blocked users", -1, { response }); return false; } diff --git a/frontend/src/ts/elements/alerts.ts b/frontend/src/ts/elements/alerts.ts index 295c292fd960..e94aa46125ba 100644 --- a/frontend/src/ts/elements/alerts.ts +++ b/frontend/src/ts/elements/alerts.ts @@ -6,7 +6,12 @@ import * as NotificationEvent from "../observables/notification-event"; import * as BadgeController from "../controllers/badge-controller"; import * as Notifications from "../elements/notifications"; import * as ConnectionState from "../states/connection"; -import { escapeHTML } from "../utils/misc"; +import { + applyReducedMotion, + createErrorMessage, + escapeHTML, + promiseAnimate, +} from "../utils/misc"; import AnimatedModal from "../utils/animated-modal"; import { updateXp as accountPageUpdateProfile } from "./profile"; import { MonkeyMail } from "@monkeytype/schemas/users"; @@ -29,10 +34,17 @@ let mailToMarkRead: string[] = []; let mailToDelete: string[] = []; type State = { - notifications: { message: string; level: number; customTitle?: string }[]; + notifications: { + id: string; + title: string; + message: string; + level: number; + details?: string | object; + }[]; psas: { message: string; level: number }[]; }; +let notificationId = 0; const state: State = { notifications: [], psas: [], @@ -289,28 +301,29 @@ function fillNotifications(): void { } else { notificationHistoryListEl.empty(); for (const n of state.notifications) { - const { message, level, customTitle } = n; - let title = "Notice"; + const { message, level, title } = n; + let levelClass = "sub"; if (level === -1) { levelClass = "error"; - title = "Error"; } else if (level === 1) { levelClass = "main"; - title = "Success"; - } - - if (customTitle !== undefined) { - title = customTitle; } notificationHistoryListEl.prependHtml(` -
+
${title}
${escapeHTML(message)}
+
+ ${ + n.details !== undefined + ? `` + : `` + } +
`); } @@ -396,15 +409,89 @@ function updateClaimDeleteAllButton(): void { } } +async function copyNotificationToClipboard(target: HTMLElement): Promise { + const id = (target as HTMLElement | null) + ?.closest(".item") + ?.getAttribute("data-id") + ?.toString(); + + if (id === undefined) { + throw new Error("Notification ID is undefined"); + } + const notification = state.notifications.find((it) => it.id === id); + if (notification === undefined) return; + + const icon = target.querySelector("i") as HTMLElement; + + try { + await navigator.clipboard.writeText( + JSON.stringify( + { + title: notification.title, + message: notification.message, + details: notification.details, + }, + null, + 4, + ), + ); + + const duration = applyReducedMotion(100); + + await promiseAnimate(icon, { + scale: [1, 0.8], + opacity: [1, 0], + duration, + }); + icon.classList.remove("fa-clipboard"); + icon.classList.add("fa-check", "highlight"); + await promiseAnimate(icon, { + scale: [0.8, 1], + opacity: [0, 1], + duration, + }); + + await promiseAnimate(icon, { + scale: [1, 0.8], + opacity: [1, 0], + delay: 3000, + duration, + }); + icon.classList.remove("fa-check", "highlight"); + icon.classList.add("fa-clipboard"); + + await promiseAnimate(icon, { + scale: [0.8, 1], + opacity: [0, 1], + duration, + }); + } catch (e: unknown) { + const msg = createErrorMessage(e, "Could not copy to clipboard"); + Notifications.add(msg, -1); + } +} + qs("header nav .showAlerts")?.on("click", () => { void show(); }); -NotificationEvent.subscribe((message, level, customTitle) => { +NotificationEvent.subscribe((message, level, options) => { + let title = "Notice"; + if (level === -1) { + title = "Error"; + } else if (level === 1) { + title = "Success"; + } + if (options.customTitle !== undefined) { + title = options.customTitle; + } + state.notifications.push({ + id: (notificationId++).toString(), + title, message, level, - customTitle, + details: options.details, }); if (state.notifications.length > 25) { state.notifications.shift(); @@ -495,5 +582,11 @@ const modal = new AnimatedModal({ markReadAlert(id); }); + + alertsPopupEl + .qs(".notificationHistory .list") + ?.onChild("click", ".item .buttons .copyNotification", (e) => { + void copyNotificationToClipboard(e.target as HTMLElement); + }); }, }); diff --git a/frontend/src/ts/elements/notifications.ts b/frontend/src/ts/elements/notifications.ts index c15e0638ffa3..e85593a5d57d 100644 --- a/frontend/src/ts/elements/notifications.ts +++ b/frontend/src/ts/elements/notifications.ts @@ -5,6 +5,7 @@ import * as NotificationEvent from "../observables/notification-event"; import { convertRemToPixels } from "../utils/numbers"; import { animate } from "animejs"; import { qsr } from "../utils/dom"; +import { CommonResponsesType } from "@monkeytype/contracts/util/api"; const notificationCenter = qsr("#notificationCenter"); const notificationCenterHistory = notificationCenter.qsr(".history"); @@ -107,6 +108,7 @@ class Notification { visibleStickyNotifications++; updateClearAllButton(); } + notificationCenterHistory.prependHtml(`
${icon}
${title}
${this.message}
@@ -270,6 +272,8 @@ export type AddNotificationOptions = { customIcon?: string; closeCallback?: () => void; allowHTML?: boolean; + details?: object | string; + response?: CommonResponsesType; }; export function add( @@ -277,7 +281,24 @@ export function add( level = 0, options: AddNotificationOptions = {}, ): void { - NotificationEvent.dispatch(message, level, options.customTitle); + let details = options.details; + + if (options.response !== undefined) { + details = { + status: options.response.status, + additionalDetails: options.details, + validationErrors: + options.response.status === 422 + ? options.response.body.validationErrors + : undefined, + }; + message = message + ": " + options.response.body.message; + } + + NotificationEvent.dispatch(message, level, { + customTitle: options.customTitle, + details, + }); new Notification( "notification", diff --git a/frontend/src/ts/modals/dev-options.ts b/frontend/src/ts/modals/dev-options.ts index 199f530ab5cd..6c254688ada8 100644 --- a/frontend/src/ts/modals/dev-options.ts +++ b/frontend/src/ts/modals/dev-options.ts @@ -32,6 +32,7 @@ async function setup(modalEl: HTMLElement): Promise { }); Notifications.add("This is a test", -1, { duration: 0, + details: { test: true, error: "Example error message" }, }); void modal.hide(); }); diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 694e9b69ffc8..7f3771994905 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -284,7 +284,7 @@ async function apply(): Promise { if (response.status !== 200 || response.body.data === null) { Notifications.add( - "Failed to add preset: " + + "Failed to add preset" + response.body.message.replace(presetName, propPresetName), -1, ); @@ -325,7 +325,7 @@ async function apply(): Promise { }); if (response.status !== 200) { - Notifications.add("Failed to edit preset: " + response.body.message, -1); + Notifications.add("Failed to edit preset", -1, { response }); } else { Notifications.add("Preset updated", 1); @@ -344,10 +344,7 @@ async function apply(): Promise { const response = await Ape.presets.delete({ params: { presetId } }); if (response.status !== 200) { - Notifications.add( - "Failed to remove preset: " + response.body.message, - -1, - ); + Notifications.add("Failed to remove preset", -1, { response }); } else { Notifications.add("Preset removed", 1); snapshotPresets.forEach((preset: SnapshotPreset, index: number) => { diff --git a/frontend/src/ts/modals/edit-profile.ts b/frontend/src/ts/modals/edit-profile.ts index 633c89d39711..3eed5708533f 100644 --- a/frontend/src/ts/modals/edit-profile.ts +++ b/frontend/src/ts/modals/edit-profile.ts @@ -180,7 +180,7 @@ async function updateProfile(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to update profile: " + response.body.message, -1); + Notifications.add("Failed to update profile", -1, { response }); return; } diff --git a/frontend/src/ts/modals/edit-result-tags.ts b/frontend/src/ts/modals/edit-result-tags.ts index 6ce533a4349c..f3a65df6119e 100644 --- a/frontend/src/ts/modals/edit-result-tags.ts +++ b/frontend/src/ts/modals/edit-result-tags.ts @@ -121,10 +121,7 @@ async function save(): Promise { state.tags = state.tags.filter((el) => el !== undefined); if (response.status !== 200) { - Notifications.add( - "Failed to update result tags: " + response.body.message, - -1, - ); + Notifications.add("Failed to update result tags", -1, { response }); return; } diff --git a/frontend/src/ts/modals/edit-tag.ts b/frontend/src/ts/modals/edit-tag.ts index 880531a57357..5582aa14d59d 100644 --- a/frontend/src/ts/modals/edit-tag.ts +++ b/frontend/src/ts/modals/edit-tag.ts @@ -37,6 +37,7 @@ const actionModals: Record = { message: "Failed to add tag: " + response.body.message.replace(tagName, propTagName), + notificationOptions: { response }, }; } @@ -83,7 +84,8 @@ const actionModals: Record = { if (response.status !== 200) { return { status: -1, - message: "Failed to edit tag: " + response.body.message, + message: "Failed to edit tag", + notificationOptions: { response }, }; } @@ -113,7 +115,8 @@ const actionModals: Record = { if (response.status !== 200) { return { status: -1, - message: "Failed to remove tag: " + response.body.message, + message: "Failed to remove tag", + notificationOptions: { response }, }; } @@ -143,7 +146,8 @@ const actionModals: Record = { if (response.status !== 200) { return { status: -1, - message: "Failed to clear tag pb: " + response.body.message, + message: "Failed to clear tag pb", + notificationOptions: { response }, }; } diff --git a/frontend/src/ts/modals/quote-approve.ts b/frontend/src/ts/modals/quote-approve.ts index 988c7c5a3728..3fd7a79febac 100644 --- a/frontend/src/ts/modals/quote-approve.ts +++ b/frontend/src/ts/modals/quote-approve.ts @@ -98,7 +98,7 @@ async function getQuotes(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to get new quotes: " + response.body.message, -1); + Notifications.add("Failed to get new quotes", -1, { response }); return; } @@ -160,7 +160,7 @@ async function approveQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); quote.find("textarea, input").prop("disabled", false); - Notifications.add("Failed to approve quote: " + response.body.message, -1); + Notifications.add("Failed to approve quote", -1, { response }); return; } @@ -184,7 +184,7 @@ async function refuseQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); quote.find("textarea, input").prop("disabled", false); - Notifications.add("Failed to refuse quote: " + response.body.message, -1); + Notifications.add("Failed to refuse quote", -1, { response }); return; } @@ -218,7 +218,7 @@ async function editQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); quote.find("textarea, input").prop("disabled", false); - Notifications.add("Failed to approve quote: " + response.body.message, -1); + Notifications.add("Failed to approve quote", -1, { response }); return; } diff --git a/frontend/src/ts/modals/quote-rate.ts b/frontend/src/ts/modals/quote-rate.ts index 132eef7bf2c0..e22b97f91e1c 100644 --- a/frontend/src/ts/modals/quote-rate.ts +++ b/frontend/src/ts/modals/quote-rate.ts @@ -60,10 +60,7 @@ export async function getQuoteStats( Loader.hide(); if (response.status !== 200) { - Notifications.add( - "Failed to get quote ratings: " + response.body.message, - -1, - ); + Notifications.add("Failed to get quote ratings", -1, { response }); return; } @@ -156,10 +153,7 @@ async function submit(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add( - "Failed to submit quote rating: " + response.body.message, - -1, - ); + Notifications.add("Failed to submit quote rating", -1, { response }); return; } diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index 991ec10bc543..f24a423abb76 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -121,7 +121,7 @@ async function submitReport(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to report quote: " + response.body.message, -1); + Notifications.add("Failed to report quote", -1, { response }); return; } diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index 07d0befc78cc..2a2f99fb337f 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -44,7 +44,7 @@ async function submitQuote(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to submit quote: " + response.body.message, -1); + Notifications.add("Failed to submit quote", -1, { response }); return; } diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index a15a831356a9..c22d32068a75 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -288,7 +288,8 @@ list.updateEmail = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to update email: " + response.body.message, + message: "Failed to update email", + notificationOptions: { response }, }; } @@ -499,13 +500,14 @@ list.updateName = new SimpleModal({ }; } - const updateNameResponse = await Ape.users.updateName({ + const response = await Ape.users.updateName({ body: { name: newName }, }); - if (updateNameResponse.status !== 200) { + if (response.status !== 200) { return { status: -1, - message: "Failed to update name: " + updateNameResponse.body.message, + message: "Failed to update name", + notificationOptions: { response }, }; } @@ -598,7 +600,8 @@ list.updatePassword = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to update password: " + response.body.message, + message: "Failed to update password", + notificationOptions: { response }, }; } @@ -699,8 +702,8 @@ list.addPasswordAuth = new SimpleModal({ return { status: -1, message: - "Password authentication added but updating the database email failed. This shouldn't happen, please contact support. Error: " + - response.body.message, + "Password authentication added but updating the database email failed. This shouldn't happen, please contact support. Error", + notificationOptions: { response }, }; } @@ -735,12 +738,13 @@ list.deleteAccount = new SimpleModal({ } Notifications.add("Deleting all data...", 0); - const usersResponse = await Ape.users.delete(); + const response = await Ape.users.delete(); - if (usersResponse.status !== 200) { + if (response.status !== 200) { return { status: -1, - message: "Failed to delete user data: " + usersResponse.body.message, + message: "Failed to delete user data", + notificationOptions: { response }, }; } @@ -791,7 +795,8 @@ list.resetAccount = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to reset account: " + response.body.message, + message: "Failed to reset account", + notificationOptions: { response }, }; } @@ -837,7 +842,8 @@ list.optOutOfLeaderboards = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to opt out: " + response.body.message, + message: "Failed to opt out", + notificationOptions: { response }, }; } @@ -898,7 +904,8 @@ list.resetPersonalBests = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to reset personal bests: " + response.body.message, + message: "Failed to reset personal bests", + notificationOptions: { response }, }; } @@ -974,7 +981,8 @@ list.revokeAllTokens = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to revoke tokens: " + response.body.message, + message: "Failed to revoke tokens", + notificationOptions: { response }, }; } @@ -1015,7 +1023,8 @@ list.unlinkDiscord = new SimpleModal({ if (response.status !== 200) { return { status: -1, - message: "Failed to unlink Discord: " + response.body.message, + message: "Failed to unlink Discord", + notificationOptions: { response }, }; } diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts index b81cc48e9784..f2f9d90aa3c9 100644 --- a/frontend/src/ts/modals/streak-hour-offset.ts +++ b/frontend/src/ts/modals/streak-hour-offset.ts @@ -88,10 +88,7 @@ async function apply(): Promise { }); Loader.hide(); if (response.status !== 200) { - Notifications.add( - "Failed to set streak hour offset: " + response.body.message, - -1, - ); + Notifications.add("Failed to set streak hour offset", -1, { response }); } else { Notifications.add("Streak hour offset set", 1); const snap = getSnapshot() as Snapshot; diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index f54945928e8f..1878a09a7598 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -128,7 +128,7 @@ async function submitReport(): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to report user: " + response.body.message, -1); + Notifications.add("Failed to report user", -1, { response }); return; } diff --git a/frontend/src/ts/observables/notification-event.ts b/frontend/src/ts/observables/notification-event.ts index cf05dc18b12e..5def1fd9a625 100644 --- a/frontend/src/ts/observables/notification-event.ts +++ b/frontend/src/ts/observables/notification-event.ts @@ -1,7 +1,12 @@ -type SubscribeFunction = ( +export type NotificationOptions = { + customTitle?: string; + details?: object | string; +}; + +export type SubscribeFunction = ( message: string, level: number, - customTitle?: string, + options: NotificationOptions, ) => void; const subscribers: SubscribeFunction[] = []; @@ -13,11 +18,11 @@ export function subscribe(fn: SubscribeFunction): void { export function dispatch( message: string, level: number, - customTitle?: string, + options: NotificationOptions, ): void { subscribers.forEach((fn) => { try { - fn(message, level, customTitle); + fn(message, level, options); } catch (e) { console.error("Notification event subscriber threw an error"); console.error(e); diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 537725739ac9..3261938806e7 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -1107,7 +1107,7 @@ $(".pageAccount").on("click", ".miniResultChartButton", async (event) => { target.removeClass("loading"); if (response.status !== 200) { - Notifications.add("Error fetching result: " + response.body.message, -1); + Notifications.add("Error fetching result", -1, { response }); return; } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 4232665906ab..53fcdb878e6b 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1285,7 +1285,7 @@ async function saveResult( response.body.message = "Looks like your result data is using an incorrect schema. Please refresh the page to download the new update. If the problem persists, please contact support."; } - Notifications.add("Failed to save result: " + response.body.message, -1); + Notifications.add("Failed to save result", -1, { response }); return; } diff --git a/frontend/src/ts/utils/results.ts b/frontend/src/ts/utils/results.ts index fc10e6ba3cc7..2b4806f67538 100644 --- a/frontend/src/ts/utils/results.ts +++ b/frontend/src/ts/utils/results.ts @@ -14,10 +14,7 @@ export async function syncNotSignedInLastResult(uid: string): Promise { body: { result: notSignedInLastResult }, }); if (response.status !== 200) { - Notifications.add( - "Failed to save last result: " + response.body.message, - -1, - ); + Notifications.add("Failed to save last result", -1, { response }); return; } diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 5a94abd004e5..861480df008e 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -48,7 +48,7 @@ export async function linkDiscord(hashOverride: string): Promise { Loader.hide(); if (response.status !== 200) { - Notifications.add("Failed to link Discord: " + response.body.message, -1); + Notifications.add("Failed to link Discord", -1, { response }); return; } diff --git a/packages/contracts/src/util/api.ts b/packages/contracts/src/util/api.ts index 275e4b8fc606..3bd5c92d7bcd 100644 --- a/packages/contracts/src/util/api.ts +++ b/packages/contracts/src/util/api.ts @@ -77,6 +77,8 @@ export const MonkeyValidationErrorSchema = MonkeyResponseSchema.extend({ export type MonkeyValidationError = z.infer; export const MonkeyClientError = MonkeyResponseSchema; +export type MonkeyClientErrorType = z.infer; + export const MonkeyServerError = MonkeyClientError.extend({ errorId: z.string(), uid: z.string().optional(), @@ -130,3 +132,17 @@ export const CommonResponses = { "Endpoint disabled or server is under maintenance", ), }; + +export type CommonResponsesType = + | { + status: 400 | 401 | 403 | 429 | 470 | 471 | 472 | 479; + body: MonkeyClientErrorType; + } + | { + status: 422; + body: MonkeyValidationError; + } + | { + status: 500 | 503; + body: MonkeyServerErrorType; + };