Skip to content

Commit e50d5e9

Browse files
fehmerMiodec
andauthored
feat(settings): add deep links to specific sections (@fehmer) (monkeytypegame#6661)
Allow to deep-link to a specific config setting. Users in discord often ask how to enable a specific setting. Instead of guiding them manually to the correct settings we can use a deep-link: https://github.com/user-attachments/assets/90b99660-13ae-49bf-8ff2-c5e6da290070 Added a link next to the group on mouse over to copy the deep link to the clipboard: ![image](https://github.com/user-attachments/assets/d389754e-f43a-4638-b18f-729e818ef170) --------- Co-authored-by: Miodec <[email protected]>
1 parent ee6a929 commit e50d5e9

File tree

8 files changed

+329
-13
lines changed

8 files changed

+329
-13
lines changed

frontend/src/html/pages/settings.html

Lines changed: 231 additions & 6 deletions
Large diffs are not rendered by default.

frontend/src/styles/animations.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,12 @@
111111
bottom: 0%;
112112
}
113113
}
114+
115+
@keyframes flashBorder {
116+
0% {
117+
box-shadow: 0 0 0 0.2em var(--main-color);
118+
}
119+
100% {
120+
box-shadow: 0 0 0 0.2em transparent;
121+
}
122+
}

frontend/src/styles/settings.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,26 @@
489489
}
490490
}
491491
}
492+
493+
&.highlight {
494+
margin: -1em;
495+
padding: 1em;
496+
animation: flashBorder 4s ease-in;
497+
border-radius: var(--roundness);
498+
}
499+
500+
.groupTitle button.text {
501+
padding: 0;
502+
margin-left: 0.5em;
503+
align-self: center;
504+
opacity: 0;
505+
.fas {
506+
margin-right: 0;
507+
}
508+
}
509+
&:hover .groupTitle button.text {
510+
opacity: 1;
511+
}
492512
}
493513
}
494514

frontend/src/ts/elements/settings/settings-group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class SettingsGroup<T extends ConfigValue> {
5454
this.elements = [el];
5555
} else if (this.mode === "button") {
5656
const els = document.querySelectorAll(`
57-
.pageSettings .section[data-config-name=${this.configName}] button`);
57+
.pageSettings .section[data-config-name=${this.configName}] .buttons button, .pageSettings .section[data-config-name=${this.configName}] .inputs button`);
5858

5959
if (els.length === 0) {
6060
throw new Error(`Failed to find a button element for ${configName}`);

frontend/src/ts/event-handlers/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ settingsPage
1414
});
1515

1616
settingsPage
17-
?.querySelector(".section.updateCookiePreferences button")
17+
?.querySelector(".section.updateCookiePreferences .buttons button")
1818
?.addEventListener("click", () => {
1919
CookiesModal.show(true);
2020
});

frontend/src/ts/pages/settings.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as Notifications from "../elements/notifications";
1313
import * as ImportExportSettingsModal from "../modals/import-export-settings";
1414
import * as ConfigEvent from "../observables/config-event";
1515
import * as ActivePage from "../states/active-page";
16-
import Page from "./page";
16+
import { PageWithUrlParams } from "./page";
1717
import { isAuthenticated } from "../firebase";
1818
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
1919
import SlimSelect from "slim-select";
@@ -25,6 +25,7 @@ import {
2525
ThemeName,
2626
CustomLayoutFluid,
2727
FunboxName,
28+
ConfigKeySchema,
2829
} from "@monkeytype/contracts/schemas/configs";
2930
import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox";
3031
import { getActiveFunboxNames } from "../test/funbox/list";
@@ -37,6 +38,7 @@ import { areSortedArraysEqual, areUnsortedArraysEqual } from "../utils/arrays";
3738
import { LayoutName } from "@monkeytype/contracts/schemas/layouts";
3839
import { LanguageGroupNames, LanguageGroups } from "../constants/languages";
3940
import { Language } from "@monkeytype/contracts/schemas/languages";
41+
import { z } from "zod";
4042

4143
let settingsInitialized = false;
4244

@@ -46,6 +48,24 @@ let customPolyglotSelect: SlimSelect | undefined;
4648

4749
export const groups: SettingsGroups<ConfigValue> = {};
4850

51+
const HighlightSchema = ConfigKeySchema.or(
52+
z.enum([
53+
"resetSettings",
54+
"updateCookiePreferences",
55+
"importexportSettings",
56+
"theme",
57+
"presets",
58+
"tags",
59+
])
60+
);
61+
type Highlight = z.infer<typeof HighlightSchema>;
62+
63+
const StateSchema = z
64+
.object({
65+
highlight: HighlightSchema,
66+
})
67+
.partial();
68+
4969
async function initGroups(): Promise<void> {
5070
groups["smoothCaret"] = new SettingsGroup(
5171
"smoothCaret",
@@ -1001,7 +1021,7 @@ $(".pageSettings .section[data-config-name='minBurst']").on(
10011021
);
10021022

10031023
//funbox
1004-
$(".pageSettings .section[data-config-name='funbox']").on(
1024+
$(".pageSettings .section[data-config-name='funbox'] .buttons").on(
10051025
"click",
10061026
"button",
10071027
(e) => {
@@ -1333,6 +1353,40 @@ function getThemeDropdownData(
13331353
}));
13341354
}
13351355

1356+
function handleHighlightSection(highlight: Highlight): void {
1357+
const element = document.querySelector(
1358+
`[data-config-name="${highlight}"] .groupTitle,[data-section-id="${highlight}"] .groupTitle`
1359+
);
1360+
1361+
if (element !== null) {
1362+
setTimeout(() => {
1363+
element.scrollIntoView({ block: "center", behavior: "auto" });
1364+
element.parentElement?.classList.remove("highlight");
1365+
element.parentElement?.classList.add("highlight");
1366+
}, 250);
1367+
}
1368+
}
1369+
1370+
$(".pageSettings .section .groupTitle button").on("click", (e) => {
1371+
const section = e.target.parentElement?.parentElement;
1372+
const configName = (section?.dataset?.["configName"] ??
1373+
section?.dataset?.["sectionId"]) as Highlight | undefined;
1374+
if (configName === undefined) {
1375+
return;
1376+
}
1377+
1378+
page.setUrlParams({ highlight: configName });
1379+
1380+
navigator.clipboard
1381+
.writeText(window.location.toString())
1382+
.then(() => {
1383+
Notifications.add("Link copied to clipboard", 1);
1384+
})
1385+
.catch((e: unknown) => {
1386+
Notifications.add("Failed to copy to clipboard: " + e, -1);
1387+
});
1388+
});
1389+
13361390
ConfigEvent.subscribe((eventKey, eventValue) => {
13371391
if (eventKey === "fullConfigChange") setEventDisabled(true);
13381392
if (eventKey === "fullConfigChangeFinished") setEventDisabled(false);
@@ -1352,18 +1406,22 @@ ConfigEvent.subscribe((eventKey, eventValue) => {
13521406
}
13531407
});
13541408

1355-
export const page = new Page({
1409+
export const page = new PageWithUrlParams({
13561410
id: "settings",
13571411
element: $(".page.pageSettings"),
13581412
path: "/settings",
1413+
urlParamsSchema: StateSchema,
13591414
afterHide: async (): Promise<void> => {
13601415
Skeleton.remove("pageSettings");
13611416
},
1362-
beforeShow: async (): Promise<void> => {
1417+
beforeShow: async (options): Promise<void> => {
13631418
Skeleton.append("pageSettings", "main");
13641419
await UpdateConfig.loadPromise;
13651420
await fillSettingsPage();
13661421
await update();
1422+
if (options.urlParams?.highlight !== undefined) {
1423+
handleHighlightSection(options.urlParams.highlight);
1424+
}
13671425
},
13681426
});
13691427

monkeytype.code-workspace

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
"testing.automaticallyOpenTestResults": "neverOpen",
4444
"[json]": {
4545
"editor.defaultFormatter": "esbenp.prettier-vscode"
46+
},
47+
"[html]": {
48+
"editor.defaultFormatter": "esbenp.prettier-vscode"
4649
}
4750
},
4851

packages/contracts/src/schemas/configs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,8 @@ export const ConfigSchema = z
438438
.strict();
439439

440440
export type Config = z.infer<typeof ConfigSchema>;
441-
export type ConfigKey = keyof Config;
441+
export const ConfigKeySchema = ConfigSchema.keyof();
442+
export type ConfigKey = z.infer<typeof ConfigKeySchema>;
442443
export type ConfigValue = Config[keyof Config];
443444

444445
export const PartialConfigSchema = ConfigSchema.partial();

0 commit comments

Comments
 (0)