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