mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-29 02:07:55 +08:00
refactor: move themes list to typescript (@fehmer) (#6489)
Co-authored-by: fehmer <3728838+fehmer@users.noreply.github.com>
This commit is contained in:
parent
dc6d4518a9
commit
5ab7bfb438
19 changed files with 1544 additions and 1614 deletions
|
|
@ -36,16 +36,29 @@ Then add this code to your file:
|
|||
Here is an image showing what all the properties correspond to:
|
||||
<img width="1552" alt="Screenshot showing the page elements controlled by each color property" src="https://user-images.githubusercontent.com/83455454/149196967-abb69795-0d38-466b-a867-5aaa46452976.png">
|
||||
|
||||
Change the corresponding hex codes to create your theme. Then, go to `./frontend/static/themes/_list.json` and add the following code to the very end of the file (inside the square brackets):
|
||||
Change the corresponding hex codes to create your theme.
|
||||
Then, go to `./packages/contracts/src/schemas/themes.ts` and add your new theme name at the _end_ of the `ThemeNameSchema` enum. Make sure to end the line with a comma.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "theme_name",
|
||||
"bgColor": "#ffffff",
|
||||
"mainColor": "#ffffff",
|
||||
"subColor": "#ffffff",
|
||||
"textColor": "#ffffff"
|
||||
```typescript
|
||||
export const ThemeNameSchema = z.enum([
|
||||
"8008",
|
||||
"80s_after_dark",
|
||||
...
|
||||
"your_theme_name",
|
||||
```
|
||||
|
||||
Then, go to `./frontend/src/ts/constants/themes.ts` and add the following code to the _end_ of the `themes` object near to the very end of the file:
|
||||
|
||||
```typescript
|
||||
export const themes: Record<ThemeName, Omit<Theme, "name">> = {
|
||||
...
|
||||
your_theme_name: {
|
||||
bgColor "#ffffff",
|
||||
mainColor "#ffffff",
|
||||
subColor "#ffffff",
|
||||
textColor "#ffffff"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Make sure the name you put matches the name of the file you created (without the `.css` file extension). Add the text color and background color of your theme to their respective fields.
|
||||
|
|
|
|||
38
frontend/__tests__/constants/themes.spec.ts
Normal file
38
frontend/__tests__/constants/themes.spec.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { readdirSync } from "fs";
|
||||
import { ThemesList } from "../../src/ts/constants/themes";
|
||||
|
||||
describe("themes", () => {
|
||||
it("should not have duplicates", () => {
|
||||
const duplicates = ThemesList.filter(
|
||||
(item, index) => ThemesList.indexOf(item) !== index
|
||||
);
|
||||
expect(duplicates).toEqual([]);
|
||||
});
|
||||
it("should have all related css files", () => {
|
||||
const themeFiles = listThemeFiles();
|
||||
|
||||
const missingThemeFiles = ThemesList.filter(
|
||||
(it) => !themeFiles.includes(it.name)
|
||||
).map((it) => `fontend/static/themes/${it}.css`);
|
||||
|
||||
expect(missingThemeFiles, "missing theme css files").toEqual([]);
|
||||
});
|
||||
it("should not have additional css files", () => {
|
||||
const themeFiles = listThemeFiles();
|
||||
|
||||
const additionalThemeFiles = themeFiles
|
||||
.filter((it) => !ThemesList.some((theme) => theme.name === it))
|
||||
.map((it) => `fontend/static/themes/${it}.css`);
|
||||
|
||||
expect(
|
||||
additionalThemeFiles,
|
||||
"additional theme css files not declared in frontend/src/ts/constants/themes.ts"
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function listThemeFiles() {
|
||||
return readdirSync(import.meta.dirname + "/../../static/themes").map((it) =>
|
||||
it.substring(0, it.length - 4)
|
||||
);
|
||||
}
|
||||
|
|
@ -328,11 +328,9 @@ describe("Config", () => {
|
|||
});
|
||||
it("setFavThemes", () => {
|
||||
expect(Config.setFavThemes([])).toBe(true);
|
||||
expect(Config.setFavThemes(["test"])).toBe(true);
|
||||
expect(Config.setFavThemes([stringOfLength(50)])).toBe(true);
|
||||
|
||||
expect(Config.setFavThemes(["8008", "80s_after_dark"])).toBe(true);
|
||||
expect(Config.setFavThemes(["test"] as any)).toBe(false);
|
||||
expect(Config.setFavThemes("invalid" as any)).toBe(false);
|
||||
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
|
||||
});
|
||||
it("setFunbox", () => {
|
||||
expect(Config.setFunbox(["mirror"])).toBe(true);
|
||||
|
|
@ -407,29 +405,20 @@ describe("Config", () => {
|
|||
it("setTheme", () => {
|
||||
expect(Config.setTheme("serika")).toBe(true);
|
||||
expect(Config.setTheme("serika_dark")).toBe(true);
|
||||
expect(Config.setTheme(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setTheme("serika dark")).toBe(false);
|
||||
expect(Config.setTheme("serika-dark")).toBe(false);
|
||||
expect(Config.setTheme(stringOfLength(51))).toBe(false);
|
||||
expect(Config.setTheme("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setThemeLight", () => {
|
||||
expect(Config.setThemeLight("serika")).toBe(true);
|
||||
expect(Config.setThemeLight("serika_dark")).toBe(true);
|
||||
expect(Config.setThemeLight(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setThemeLight("serika dark")).toBe(false);
|
||||
expect(Config.setThemeLight("serika-dark")).toBe(false);
|
||||
expect(Config.setThemeLight(stringOfLength(51))).toBe(false);
|
||||
expect(Config.setThemeLight("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setThemeDark", () => {
|
||||
expect(Config.setThemeDark("serika")).toBe(true);
|
||||
expect(Config.setThemeDark("serika_dark")).toBe(true);
|
||||
expect(Config.setThemeDark(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setThemeDark("serika dark")).toBe(false);
|
||||
expect(Config.setThemeDark("serika-dark")).toBe(false);
|
||||
expect(Config.setThemeDark(stringOfLength(51))).toBe(false);
|
||||
expect(Config.setThemeDark("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setLanguage", () => {
|
||||
expect(Config.setLanguage("english")).toBe(true);
|
||||
|
|
|
|||
|
|
@ -47,46 +47,6 @@ function validateOthers() {
|
|||
return reject(new Error(fontsValidator.errors[0].message));
|
||||
}
|
||||
|
||||
//themes
|
||||
const themesData = JSON.parse(
|
||||
fs.readFileSync("./static/themes/_list.json", {
|
||||
encoding: "utf8",
|
||||
flag: "r",
|
||||
})
|
||||
);
|
||||
const themesSchema = {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
bgColor: { type: "string" },
|
||||
mainColor: { type: "string" },
|
||||
},
|
||||
required: ["name", "bgColor", "mainColor"],
|
||||
},
|
||||
};
|
||||
const themesValidator = ajv.compile(themesSchema);
|
||||
if (themesValidator(themesData)) {
|
||||
console.log("Themes list JSON schema is \u001b[32mvalid\u001b[0m");
|
||||
} else {
|
||||
console.log("Themes list JSON schema is \u001b[31minvalid\u001b[0m");
|
||||
return reject(new Error(themesValidator.errors[0].message));
|
||||
}
|
||||
//check if files exist
|
||||
for (const theme of themesData) {
|
||||
const themeName = theme.name;
|
||||
const fileName = `${themeName}.css`;
|
||||
const themePath = `./static/themes/${fileName}`;
|
||||
if (!fs.existsSync(themePath)) {
|
||||
console.log(`File ${fileName} was \u001b[31mnot found\u001b[0m`);
|
||||
// return reject(new Error(`File for theme ${themeName} does not exist`));
|
||||
return reject(
|
||||
`Could not find file ${fileName} for theme ${themeName} - make sure the file exists and is named correctly`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//challenges
|
||||
const challengesSchema = {
|
||||
type: "array",
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ import CustomThemesListCommands from "./lists/custom-themes-list";
|
|||
import PresetsCommands from "./lists/presets";
|
||||
import LayoutsCommands from "./lists/layouts";
|
||||
import FunboxCommands from "./lists/funbox";
|
||||
import ThemesCommands, { update as updateThemesCommands } from "./lists/themes";
|
||||
import ThemesCommands from "./lists/themes";
|
||||
import LoadChallengeCommands, {
|
||||
update as updateLoadChallengeCommands,
|
||||
} from "./lists/load-challenge";
|
||||
|
|
@ -130,17 +130,6 @@ fontsPromise
|
|||
);
|
||||
});
|
||||
|
||||
const themesPromise = JSONData.getThemesList();
|
||||
themesPromise
|
||||
.then((themes) => {
|
||||
updateThemesCommands(themes);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to update themes commands")
|
||||
);
|
||||
});
|
||||
|
||||
const challengesPromise = JSONData.getChallengeList();
|
||||
challengesPromise
|
||||
.then((challenges) => {
|
||||
|
|
@ -501,12 +490,7 @@ export function doesListExist(listName: string): boolean {
|
|||
export async function getList(
|
||||
listName: ListsObjectKeys
|
||||
): Promise<CommandsSubgroup> {
|
||||
await Promise.allSettled([
|
||||
languagesPromise,
|
||||
fontsPromise,
|
||||
themesPromise,
|
||||
challengesPromise,
|
||||
]);
|
||||
await Promise.allSettled([languagesPromise, fontsPromise, challengesPromise]);
|
||||
const list = lists[listName];
|
||||
if (!list) {
|
||||
Notifications.add(`List not found: ${listName}`, -1);
|
||||
|
|
@ -547,12 +531,7 @@ export function getTopOfStack(): CommandsSubgroup {
|
|||
|
||||
let singleList: CommandsSubgroup | undefined;
|
||||
export async function getSingleSubgroup(): Promise<CommandsSubgroup> {
|
||||
await Promise.allSettled([
|
||||
languagesPromise,
|
||||
fontsPromise,
|
||||
themesPromise,
|
||||
challengesPromise,
|
||||
]);
|
||||
await Promise.allSettled([languagesPromise, fontsPromise, challengesPromise]);
|
||||
|
||||
const singleCommands: Command[] = [];
|
||||
for (const command of commands.list) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ThemeName } from "@monkeytype/contracts/schemas/configs";
|
||||
import Config, * as UpdateConfig from "../../config";
|
||||
import { randomTheme } from "../../controllers/theme-controller";
|
||||
import { Command } from "../types";
|
||||
|
|
@ -10,14 +11,14 @@ const commands: Command[] = [
|
|||
available: (): boolean => {
|
||||
return (
|
||||
!Config.customTheme &&
|
||||
!Config.favThemes.includes(randomTheme ?? Config.theme)
|
||||
!Config.favThemes.includes((randomTheme as ThemeName) ?? Config.theme)
|
||||
);
|
||||
},
|
||||
exec: (): void => {
|
||||
const { theme, favThemes, customTheme } = Config;
|
||||
const themeToUpdate = randomTheme ?? theme;
|
||||
if (!customTheme && !favThemes.includes(themeToUpdate)) {
|
||||
UpdateConfig.setFavThemes([...favThemes, themeToUpdate]);
|
||||
if (!customTheme && !favThemes.includes(themeToUpdate as ThemeName)) {
|
||||
UpdateConfig.setFavThemes([...favThemes, themeToUpdate as ThemeName]);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -28,12 +29,12 @@ const commands: Command[] = [
|
|||
available: (): boolean => {
|
||||
return (
|
||||
!Config.customTheme &&
|
||||
Config.favThemes.includes(randomTheme ?? Config.theme)
|
||||
Config.favThemes.includes((randomTheme as ThemeName) ?? Config.theme)
|
||||
);
|
||||
},
|
||||
exec: (): void => {
|
||||
const { theme, favThemes, customTheme } = Config;
|
||||
const themeToUpdate = randomTheme ?? theme;
|
||||
const themeToUpdate = (randomTheme as ThemeName) ?? theme;
|
||||
if (!customTheme && favThemes.includes(themeToUpdate)) {
|
||||
UpdateConfig.setFavThemes([
|
||||
...favThemes.filter((t) => t !== themeToUpdate),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,37 @@ import Config, * as UpdateConfig from "../../config";
|
|||
import { capitalizeFirstLetterOfEachWord } from "../../utils/strings";
|
||||
import * as ThemeController from "../../controllers/theme-controller";
|
||||
import { Command, CommandsSubgroup } from "../types";
|
||||
import { Theme } from "../../utils/json-data";
|
||||
import { Theme, ThemesList } from "../../constants/themes";
|
||||
import { not } from "@monkeytype/util/predicates";
|
||||
|
||||
const isFavorite = (theme: Theme): boolean =>
|
||||
Config.favThemes.includes(theme.name);
|
||||
|
||||
const subgroup: CommandsSubgroup = {
|
||||
title: "Theme...",
|
||||
configKey: "theme",
|
||||
list: [],
|
||||
list: [
|
||||
...ThemesList.filter(isFavorite),
|
||||
...ThemesList.filter(not(isFavorite)),
|
||||
].map((theme: Theme) => ({
|
||||
id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name),
|
||||
display: theme.name.replace(/_/g, " "),
|
||||
configValue: theme.name,
|
||||
// customStyle: `color:${theme.mainColor};background:${theme.bgColor};`,
|
||||
customData: {
|
||||
mainColor: theme.mainColor,
|
||||
bgColor: theme.bgColor,
|
||||
subColor: theme.subColor,
|
||||
textColor: theme.textColor,
|
||||
},
|
||||
hover: (): void => {
|
||||
// previewTheme(theme.name);
|
||||
ThemeController.preview(theme.name);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme.name);
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const commands: Command[] = [
|
||||
|
|
@ -19,54 +44,4 @@ const commands: Command[] = [
|
|||
},
|
||||
];
|
||||
|
||||
function update(themes: Theme[]): void {
|
||||
subgroup.list = [];
|
||||
const favs: Command[] = [];
|
||||
themes.forEach((theme) => {
|
||||
if (Config.favThemes.includes(theme.name)) {
|
||||
favs.push({
|
||||
id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name),
|
||||
display: theme.name.replace(/_/g, " "),
|
||||
configValue: theme.name,
|
||||
// customStyle: `color:${theme.mainColor};background:${theme.bgColor};`,
|
||||
customData: {
|
||||
mainColor: theme.mainColor,
|
||||
bgColor: theme.bgColor,
|
||||
subColor: theme.subColor,
|
||||
textColor: theme.textColor,
|
||||
},
|
||||
hover: (): void => {
|
||||
// previewTheme(theme.name);
|
||||
ThemeController.preview(theme.name);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme.name);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
subgroup.list.push({
|
||||
id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name),
|
||||
display: theme.name.replace(/_/g, " "),
|
||||
configValue: theme.name,
|
||||
// customStyle: `color:${theme.mainColor};background:${theme.bgColor}`,
|
||||
customData: {
|
||||
mainColor: theme.mainColor,
|
||||
bgColor: theme.bgColor,
|
||||
subColor: theme.subColor,
|
||||
textColor: theme.textColor,
|
||||
},
|
||||
hover: (): void => {
|
||||
// previewTheme(theme.name);
|
||||
ThemeController.preview(theme.name);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme.name);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
subgroup.list = [...favs, ...subgroup.list];
|
||||
}
|
||||
|
||||
export default commands;
|
||||
export { update };
|
||||
|
|
|
|||
1135
frontend/src/ts/constants/themes.ts
Normal file
1135
frontend/src/ts/constants/themes.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,7 @@ import {
|
|||
import {
|
||||
Config as ConfigType,
|
||||
Difficulty,
|
||||
ThemeName,
|
||||
FunboxName,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { Mode } from "@monkeytype/contracts/schemas/shared";
|
||||
|
|
@ -282,7 +283,7 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
UpdateConfig.setMode("custom", true);
|
||||
UpdateConfig.setDifficulty("normal", true);
|
||||
if (challenge.parameters[1] !== null) {
|
||||
UpdateConfig.setTheme(challenge.parameters[1] as string);
|
||||
UpdateConfig.setTheme(challenge.parameters[1] as ThemeName);
|
||||
}
|
||||
if (challenge.parameters[2] !== null) {
|
||||
void Funbox.activate(challenge.parameters[2] as FunboxName[]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import * as ThemeColors from "../elements/theme-colors";
|
|||
import * as ChartController from "./chart-controller";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as Arrays from "../utils/arrays";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
import { isColorDark, isColorLight } from "../utils/colors";
|
||||
import Config, { setAutoSwitchTheme, setCustomTheme } from "../config";
|
||||
import * as BackgroundFilter from "../elements/custom-background-filter";
|
||||
|
|
@ -11,9 +10,10 @@ import * as DB from "../db";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import * as Loader from "../elements/loader";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
import { ThemeName } from "@monkeytype/contracts/schemas/configs";
|
||||
import { ThemesList } from "../constants/themes";
|
||||
|
||||
export let randomTheme: string | null = null;
|
||||
export let randomTheme: ThemeName | string | null = null;
|
||||
let isPreviewingTheme = false;
|
||||
let randomThemeIndex = 0;
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ async function apply(
|
|||
}
|
||||
|
||||
function updateFooterThemeName(nameOverride?: string): void {
|
||||
let str = Config.theme;
|
||||
let str: string = Config.theme;
|
||||
if (randomTheme !== null) str = randomTheme;
|
||||
if (Config.customTheme) str = "custom";
|
||||
if (nameOverride !== undefined && nameOverride !== "") str = nameOverride;
|
||||
|
|
@ -244,17 +244,10 @@ export async function clearPreview(applyTheme = true): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
let themesList: string[] = [];
|
||||
let themesList: (ThemeName | string)[] = [];
|
||||
|
||||
async function changeThemeList(): Promise<void> {
|
||||
const { data: themes, error } = await tryCatch(JSONData.getThemesList());
|
||||
if (error) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(error, "Failed to update random theme list")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const themes = ThemesList;
|
||||
if (Config.randomTheme === "fav" && Config.favThemes.length > 0) {
|
||||
themesList = Config.favThemes;
|
||||
} else if (Config.randomTheme === "light") {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Config, * as UpdateConfig from "../../config";
|
||||
import * as ThemeController from "../../controllers/theme-controller";
|
||||
import * as Misc from "../../utils/misc";
|
||||
import * as JSONData from "../../utils/json-data";
|
||||
import * as Colors from "../../utils/colors";
|
||||
import * as Notifications from "../notifications";
|
||||
import * as ThemeColors from "../theme-colors";
|
||||
|
|
@ -11,12 +10,15 @@ import * as DB from "../../db";
|
|||
import * as ConfigEvent from "../../observables/config-event";
|
||||
import { isAuthenticated } from "../../firebase";
|
||||
import * as ActivePage from "../../states/active-page";
|
||||
import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
import {
|
||||
CustomThemeColors,
|
||||
ThemeName,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { captureException } from "../../sentry";
|
||||
import { getSortedThemesList } from "../../constants/themes";
|
||||
|
||||
function updateActiveButton(): void {
|
||||
let activeThemeName = Config.theme;
|
||||
let activeThemeName: string = Config.theme;
|
||||
if (
|
||||
Config.randomTheme !== "off" &&
|
||||
Config.randomTheme !== "custom" &&
|
||||
|
|
@ -138,7 +140,7 @@ export async function refreshPresetButtons(): Promise<void> {
|
|||
let favThemesElHTML = "";
|
||||
let themesElHTML = "";
|
||||
|
||||
let activeThemeName = Config.theme;
|
||||
let activeThemeName: string = Config.theme;
|
||||
if (
|
||||
Config.randomTheme !== "off" &&
|
||||
Config.randomTheme !== "custom" &&
|
||||
|
|
@ -147,17 +149,7 @@ export async function refreshPresetButtons(): Promise<void> {
|
|||
activeThemeName = ThemeController.randomTheme;
|
||||
}
|
||||
|
||||
const { data: themes, error } = await tryCatch(
|
||||
JSONData.getSortedThemesList()
|
||||
);
|
||||
|
||||
if (error) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(error, "Failed to fill preset theme buttons"),
|
||||
-1
|
||||
);
|
||||
return;
|
||||
}
|
||||
const themes = getSortedThemesList();
|
||||
|
||||
//first show favourites
|
||||
if (Config.favThemes.length > 0) {
|
||||
|
|
@ -276,13 +268,13 @@ export function setCustomInputs(noThemeUpdate = false): void {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleFavourite(themeName: string): void {
|
||||
function toggleFavourite(themeName: ThemeName): void {
|
||||
if (Config.favThemes.includes(themeName)) {
|
||||
// already favourite, remove
|
||||
UpdateConfig.setFavThemes(Config.favThemes.filter((t) => t !== themeName));
|
||||
} else {
|
||||
// add to favourites
|
||||
const newList: string[] = Config.favThemes;
|
||||
const newList: ThemeName[] = Config.favThemes;
|
||||
newList.push(themeName);
|
||||
UpdateConfig.setFavThemes(newList);
|
||||
}
|
||||
|
|
@ -367,7 +359,9 @@ $(".pageSettings").on("click", " .section.themes .customTheme.button", (e) => {
|
|||
|
||||
// Handle click on favorite preset theme button
|
||||
$(".pageSettings").on("click", ".section.themes .theme .favButton", (e) => {
|
||||
const theme = $(e.currentTarget).parents(".theme.button").attr("theme");
|
||||
const theme = $(e.currentTarget)
|
||||
.parents(".theme.button")
|
||||
.attr("theme") as ThemeName;
|
||||
if (theme !== undefined) {
|
||||
toggleFavourite(theme);
|
||||
void refreshPresetButtons();
|
||||
|
|
@ -380,7 +374,7 @@ $(".pageSettings").on("click", ".section.themes .theme .favButton", (e) => {
|
|||
|
||||
// Handle click on preset theme button
|
||||
$(".pageSettings").on("click", ".section.themes .theme.button", (e) => {
|
||||
const theme = $(e.currentTarget).attr("theme");
|
||||
const theme = $(e.currentTarget).attr("theme") as ThemeName;
|
||||
if (!$(e.target).hasClass("favButton") && theme !== undefined) {
|
||||
UpdateConfig.setTheme(theme);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ import Page from "./page";
|
|||
import { isAuthenticated } from "../firebase";
|
||||
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
|
||||
import SlimSelect from "slim-select";
|
||||
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
import * as CustomBackgroundFilter from "../elements/custom-background-filter";
|
||||
import {
|
||||
ConfigValue,
|
||||
CustomBackgroundSchema,
|
||||
ThemeName,
|
||||
CustomLayoutFluid,
|
||||
FunboxName,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
|
|
@ -30,9 +30,11 @@ import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox";
|
|||
import { getActiveFunboxNames } from "../test/funbox/list";
|
||||
import { SnapshotPreset } from "../constants/default-snapshot";
|
||||
import { LayoutsList } from "../constants/layouts";
|
||||
import { DataArrayPartial, Optgroup } from "slim-select/store";
|
||||
import { DataArrayPartial, Optgroup, OptionOptional } from "slim-select/store";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
import { Theme, ThemesList } from "../constants/themes";
|
||||
import { areSortedArraysEqual, areUnsortedArraysEqual } from "../utils/arrays";
|
||||
import { LayoutName } from "@monkeytype/contracts/schemas/layouts";
|
||||
|
||||
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
|
||||
|
||||
|
|
@ -473,12 +475,8 @@ async function fillSettingsPage(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
const element = document.querySelector(
|
||||
".pageSettings .section[data-config-name='language'] select"
|
||||
) as Element;
|
||||
|
||||
new SlimSelect({
|
||||
select: element,
|
||||
select: ".pageSettings .section[data-config-name='language'] select",
|
||||
data: getLanguageDropdownData(
|
||||
languageGroups ?? [],
|
||||
(language) => language === Config.language
|
||||
|
|
@ -488,93 +486,47 @@ async function fillSettingsPage(): Promise<void> {
|
|||
},
|
||||
});
|
||||
|
||||
const layoutSelectElement = document.querySelector(
|
||||
".pageSettings .section[data-config-name='layout'] select"
|
||||
) as Element;
|
||||
const keymapLayoutSelectElement = document.querySelector(
|
||||
".pageSettings .section[data-config-name='keymapLayout'] select"
|
||||
) as Element;
|
||||
|
||||
let layoutHtml = '<option value="default">off</option>';
|
||||
let keymapLayoutHtml = '<option value="overrideSync">emulator sync</option>';
|
||||
|
||||
for (const layout of LayoutsList) {
|
||||
const optionHtml = `<option value="${layout}">${layout.replace(
|
||||
/_/g,
|
||||
" "
|
||||
)}</option>`;
|
||||
if (layout.toString() !== "korean") {
|
||||
layoutHtml += optionHtml;
|
||||
}
|
||||
if (layout.toString() !== "default") {
|
||||
keymapLayoutHtml += optionHtml;
|
||||
}
|
||||
}
|
||||
|
||||
layoutSelectElement.innerHTML = layoutHtml;
|
||||
keymapLayoutSelectElement.innerHTML = keymapLayoutHtml;
|
||||
|
||||
new SlimSelect({
|
||||
select: layoutSelectElement,
|
||||
const layoutToOption: (layout: LayoutName) => OptionOptional = (layout) => ({
|
||||
value: layout,
|
||||
text: layout.replace(/_/g, " "),
|
||||
});
|
||||
|
||||
new SlimSelect({
|
||||
select: keymapLayoutSelectElement,
|
||||
select: ".pageSettings .section[data-config-name='layout'] select",
|
||||
data: [
|
||||
{ text: "off", value: "default" },
|
||||
...LayoutsList.filter((layout) => layout !== "korean").map(
|
||||
layoutToOption
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const { data: themes, error: getThemesListError } = await tryCatch(
|
||||
JSONData.getThemesList()
|
||||
);
|
||||
if (getThemesListError) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(
|
||||
getThemesListError,
|
||||
"Failed to load themes into dropdown boxes"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const themeSelectLightElement = document.querySelector(
|
||||
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light"
|
||||
) as Element;
|
||||
const themeSelectDarkElement = document.querySelector(
|
||||
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark"
|
||||
) as Element;
|
||||
|
||||
let themeSelectLightHtml = "";
|
||||
let themeSelectDarkHtml = "";
|
||||
|
||||
if (themes) {
|
||||
for (const theme of themes) {
|
||||
const optionHtml = `<option value="${theme.name}" ${
|
||||
theme.name === Config.themeLight ? "selected" : ""
|
||||
}>${theme.name.replace(/_/g, " ")}</option>`;
|
||||
themeSelectLightHtml += optionHtml;
|
||||
|
||||
const optionDarkHtml = `<option value="${theme.name}" ${
|
||||
theme.name === Config.themeDark ? "selected" : ""
|
||||
}>${theme.name.replace(/_/g, " ")}</option>`;
|
||||
themeSelectDarkHtml += optionDarkHtml;
|
||||
}
|
||||
}
|
||||
|
||||
themeSelectLightElement.innerHTML = themeSelectLightHtml;
|
||||
themeSelectDarkElement.innerHTML = themeSelectDarkHtml;
|
||||
new SlimSelect({
|
||||
select: ".pageSettings .section[data-config-name='keymapLayout'] select",
|
||||
data: [
|
||||
{ text: "emulator sync", value: "overrideSync" },
|
||||
...LayoutsList.map(layoutToOption),
|
||||
],
|
||||
});
|
||||
|
||||
new SlimSelect({
|
||||
select: themeSelectLightElement,
|
||||
select:
|
||||
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light",
|
||||
data: getThemeDropdownData((theme) => theme.name === Config.themeLight),
|
||||
events: {
|
||||
afterChange: (newVal): void => {
|
||||
UpdateConfig.setThemeLight(newVal[0]?.value as string);
|
||||
UpdateConfig.setThemeLight(newVal[0]?.value as ThemeName);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new SlimSelect({
|
||||
select: themeSelectDarkElement,
|
||||
select:
|
||||
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark",
|
||||
data: getThemeDropdownData((theme) => theme.name === Config.themeDark),
|
||||
events: {
|
||||
afterChange: (newVal): void => {
|
||||
UpdateConfig.setThemeDark(newVal[0]?.value as string);
|
||||
UpdateConfig.setThemeDark(newVal[0]?.value as ThemeName);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1411,6 +1363,16 @@ function getLayoutfluidDropdownData(): DataArrayPartial {
|
|||
}));
|
||||
}
|
||||
|
||||
function getThemeDropdownData(
|
||||
isActive: (theme: Theme) => boolean
|
||||
): DataArrayPartial {
|
||||
return ThemesList.map((theme) => ({
|
||||
value: theme.name,
|
||||
text: theme.name.replace(/_/g, " "),
|
||||
selected: isActive(theme),
|
||||
}));
|
||||
}
|
||||
|
||||
ConfigEvent.subscribe((eventKey, eventValue) => {
|
||||
if (eventKey === "fullConfigChange") setEventDisabled(true);
|
||||
if (eventKey === "fullConfigChangeFinished") setEventDisabled(false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { FunboxName } from "@monkeytype/contracts/schemas/configs";
|
||||
import { Accents } from "../test/lazy-mode";
|
||||
import { hexToHSL } from "./colors";
|
||||
|
||||
/**
|
||||
* Fetches JSON data from the specified URL using the fetch API.
|
||||
|
|
@ -92,66 +91,6 @@ export async function getLayout(layoutName: string): Promise<Layout> {
|
|||
return await cachedFetchJson<Layout>(`/layouts/${layoutName}.json`);
|
||||
}
|
||||
|
||||
export type Theme = {
|
||||
name: string;
|
||||
bgColor: string;
|
||||
mainColor: string;
|
||||
subColor: string;
|
||||
textColor: string;
|
||||
};
|
||||
|
||||
let themesList: Theme[] | undefined;
|
||||
|
||||
/**
|
||||
* Fetches the list of themes from the server, sorting them alphabetically by name.
|
||||
* If the list has already been fetched, returns the cached list.
|
||||
* @returns A promise that resolves to the sorted list of themes.
|
||||
*/
|
||||
export async function getThemesList(): Promise<Theme[]> {
|
||||
if (!themesList) {
|
||||
let themes = await cachedFetchJson<Theme[]>("/themes/_list.json");
|
||||
|
||||
themes = themes.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase();
|
||||
const nameB = b.name.toLowerCase();
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
});
|
||||
themesList = themes;
|
||||
return themesList;
|
||||
} else {
|
||||
return themesList;
|
||||
}
|
||||
}
|
||||
|
||||
let sortedThemesList: Theme[] | undefined;
|
||||
|
||||
/**
|
||||
* Fetches the sorted list of themes from the server.
|
||||
* @returns A promise that resolves to the sorted list of themes.
|
||||
*/
|
||||
export async function getSortedThemesList(): Promise<Theme[]> {
|
||||
if (!sortedThemesList) {
|
||||
if (!themesList) {
|
||||
await getThemesList();
|
||||
}
|
||||
if (!themesList) {
|
||||
throw new Error("Themes list is undefined");
|
||||
}
|
||||
let sorted = [...themesList];
|
||||
sorted = sorted.sort((a, b) => {
|
||||
const b1 = hexToHSL(a.bgColor);
|
||||
const b2 = hexToHSL(b.bgColor);
|
||||
return b2.lgt - b1.lgt;
|
||||
});
|
||||
sortedThemesList = sorted;
|
||||
return sortedThemesList;
|
||||
} else {
|
||||
return sortedThemesList;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of languages from the server.
|
||||
* @returns A promise that resolves to the list of languages.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -56,7 +56,7 @@
|
|||
"pr-check-lint-json": "cd frontend && eslint './static/**/*.json'",
|
||||
"pr-check-quote-json": "cd frontend && npx gulp pr-check-quote-json",
|
||||
"pr-check-language-json": "cd frontend && npx gulp pr-check-language-json",
|
||||
"pr-check-other-json": "cd frontend && npx gulp pr-check-other-json && turbo test -- constants/layouts"
|
||||
"pr-check-other-json": "cd frontend && npx gulp pr-check-other-json && turbo test -- constants/layouts constants/themes"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.16.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z, ZodSchema } from "zod";
|
||||
import { LanguageSchema, token } from "./util";
|
||||
import { LanguageSchema } from "./util";
|
||||
import * as Shared from "./shared";
|
||||
import * as Themes from "./themes";
|
||||
import * as Layouts from "./layouts";
|
||||
|
||||
export const SmoothCaretSchema = z.enum(["off", "slow", "medium", "fast"]);
|
||||
|
|
@ -234,7 +235,10 @@ export const CustomThemeColorsSchema = z.tuple([
|
|||
]);
|
||||
export type CustomThemeColors = z.infer<typeof CustomThemeColorsSchema>;
|
||||
|
||||
export const FavThemesSchema = z.array(token().max(50));
|
||||
export const ThemeNameSchema = Themes.ThemeNameSchema;
|
||||
export type ThemeName = z.infer<typeof ThemeNameSchema>;
|
||||
|
||||
export const FavThemesSchema = z.array(ThemeNameSchema);
|
||||
export type FavThemes = z.infer<typeof FavThemesSchema>;
|
||||
|
||||
export const FunboxNameSchema = z.enum([
|
||||
|
|
@ -315,9 +319,6 @@ export const FontFamilySchema = z
|
|||
.regex(/^[a-zA-Z0-9_\-+.]+$/);
|
||||
export type FontFamily = z.infer<typeof FontFamilySchema>;
|
||||
|
||||
export const ThemeNameSchema = token().max(50);
|
||||
export type ThemeName = z.infer<typeof ThemeNameSchema>;
|
||||
|
||||
export const KeymapLayoutSchema = z
|
||||
.literal("overrideSync")
|
||||
.or(Layouts.LayoutNameSchema);
|
||||
|
|
|
|||
188
packages/contracts/src/schemas/themes.ts
Normal file
188
packages/contracts/src/schemas/themes.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ThemeNameSchema = z.enum([
|
||||
"8008",
|
||||
"80s_after_dark",
|
||||
"9009",
|
||||
"aether",
|
||||
"alduin",
|
||||
"alpine",
|
||||
"anti_hero",
|
||||
"arch",
|
||||
"aurora",
|
||||
"beach",
|
||||
"bento",
|
||||
"bingsu",
|
||||
"bliss",
|
||||
"blue_dolphin",
|
||||
"blueberry_dark",
|
||||
"blueberry_light",
|
||||
"botanical",
|
||||
"bouquet",
|
||||
"breeze",
|
||||
"bushido",
|
||||
"cafe",
|
||||
"camping",
|
||||
"carbon",
|
||||
"catppuccin",
|
||||
"chaos_theory",
|
||||
"cheesecake",
|
||||
"cherry_blossom",
|
||||
"comfy",
|
||||
"copper",
|
||||
"creamsicle",
|
||||
"cy_red",
|
||||
"cyberspace",
|
||||
"dark",
|
||||
"dark_magic_girl",
|
||||
"dark_note",
|
||||
"darling",
|
||||
"deku",
|
||||
"desert_oasis",
|
||||
"dev",
|
||||
"diner",
|
||||
"dino",
|
||||
"discord",
|
||||
"dmg",
|
||||
"dollar",
|
||||
"dots",
|
||||
"dracula",
|
||||
"drowning",
|
||||
"dualshot",
|
||||
"earthsong",
|
||||
"everblush",
|
||||
"evil_eye",
|
||||
"ez_mode",
|
||||
"fire",
|
||||
"fledgling",
|
||||
"fleuriste",
|
||||
"floret",
|
||||
"froyo",
|
||||
"frozen_llama",
|
||||
"fruit_chew",
|
||||
"fundamentals",
|
||||
"future_funk",
|
||||
"github",
|
||||
"godspeed",
|
||||
"graen",
|
||||
"grand_prix",
|
||||
"grape",
|
||||
"gruvbox_dark",
|
||||
"gruvbox_light",
|
||||
"hammerhead",
|
||||
"hanok",
|
||||
"hedge",
|
||||
"honey",
|
||||
"horizon",
|
||||
"husqy",
|
||||
"iceberg_dark",
|
||||
"iceberg_light",
|
||||
"incognito",
|
||||
"ishtar",
|
||||
"iv_clover",
|
||||
"iv_spade",
|
||||
"joker",
|
||||
"laser",
|
||||
"lavender",
|
||||
"leather",
|
||||
"lil_dragon",
|
||||
"lilac_mist",
|
||||
"lime",
|
||||
"luna",
|
||||
"macroblank",
|
||||
"magic_girl",
|
||||
"mashu",
|
||||
"matcha_moccha",
|
||||
"material",
|
||||
"matrix",
|
||||
"menthol",
|
||||
"metaverse",
|
||||
"metropolis",
|
||||
"mexican",
|
||||
"miami",
|
||||
"miami_nights",
|
||||
"midnight",
|
||||
"milkshake",
|
||||
"mint",
|
||||
"mizu",
|
||||
"modern_dolch",
|
||||
"modern_dolch_light",
|
||||
"modern_ink",
|
||||
"monokai",
|
||||
"moonlight",
|
||||
"mountain",
|
||||
"mr_sleeves",
|
||||
"ms_cupcakes",
|
||||
"muted",
|
||||
"nautilus",
|
||||
"nebula",
|
||||
"night_runner",
|
||||
"nord",
|
||||
"nord_light",
|
||||
"norse",
|
||||
"oblivion",
|
||||
"olive",
|
||||
"olivia",
|
||||
"onedark",
|
||||
"our_theme",
|
||||
"paper",
|
||||
"passion_fruit",
|
||||
"pastel",
|
||||
"peach_blossom",
|
||||
"peaches",
|
||||
"phantom",
|
||||
"pink_lemonade",
|
||||
"pulse",
|
||||
"purpleish",
|
||||
"rainbow_trail",
|
||||
"red_dragon",
|
||||
"red_samurai",
|
||||
"repose_dark",
|
||||
"repose_light",
|
||||
"retro",
|
||||
"retrocast",
|
||||
"rgb",
|
||||
"rose_pine",
|
||||
"rose_pine_dawn",
|
||||
"rose_pine_moon",
|
||||
"rudy",
|
||||
"ryujinscales",
|
||||
"serika",
|
||||
"serika_dark",
|
||||
"sewing_tin",
|
||||
"sewing_tin_light",
|
||||
"shadow",
|
||||
"shoko",
|
||||
"slambook",
|
||||
"snes",
|
||||
"soaring_skies",
|
||||
"solarized_dark",
|
||||
"solarized_light",
|
||||
"solarized_osaka",
|
||||
"sonokai",
|
||||
"stealth",
|
||||
"strawberry",
|
||||
"striker",
|
||||
"suisei",
|
||||
"sunset",
|
||||
"superuser",
|
||||
"sweden",
|
||||
"tangerine",
|
||||
"taro",
|
||||
"terminal",
|
||||
"terra",
|
||||
"terrazzo",
|
||||
"terror_below",
|
||||
"tiramisu",
|
||||
"trackday",
|
||||
"trance",
|
||||
"tron_orange",
|
||||
"vaporwave",
|
||||
"vesper",
|
||||
"viridescent",
|
||||
"voc",
|
||||
"vscode",
|
||||
"watermelon",
|
||||
"wavez",
|
||||
"witch_girl",
|
||||
]);
|
||||
40
packages/util/__test__/predicates.spec.ts
Normal file
40
packages/util/__test__/predicates.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { not } from "../src/predicates";
|
||||
|
||||
describe("predicates", () => {
|
||||
describe("not", () => {
|
||||
it("should not a simple boolean function", () => {
|
||||
const isTrue = (): boolean => true;
|
||||
const isFalse = not(isTrue);
|
||||
|
||||
expect(isFalse()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not a numeric predicate", () => {
|
||||
const isPositive = (num: number): boolean => num > 0;
|
||||
const isNotPositive = not(isPositive);
|
||||
|
||||
expect(isNotPositive(-5)).toBe(true);
|
||||
expect(isNotPositive(10)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not a predicate taking multiple arguments", () => {
|
||||
const containsLetter = (
|
||||
str1: string,
|
||||
str2: string,
|
||||
letter: string
|
||||
): boolean => str1.includes(letter) || str2.includes(letter);
|
||||
const doesNotContainLetter = not(containsLetter);
|
||||
|
||||
expect(doesNotContainLetter("hello", "world", "x")).toBe(true);
|
||||
expect(doesNotContainLetter("apple", "banana", "a")).toBe(false);
|
||||
});
|
||||
|
||||
it("should preserve type safety", () => {
|
||||
const isEven = (num: number): boolean => num % 2 === 0;
|
||||
const isOdd = not(isEven);
|
||||
|
||||
expect(isOdd(3)).toBe(true);
|
||||
expect(isOdd(4)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
12
packages/util/src/predicates.ts
Normal file
12
packages/util/src/predicates.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type Predicate<Args extends unknown[]> = (...args: Args) => boolean;
|
||||
/**
|
||||
* Negates a predicate function, returning a new function that returns the opposite boolean result.
|
||||
*
|
||||
* @param predicate - The function to negate.
|
||||
* @returns A new function that returns the negated boolean result.
|
||||
*/
|
||||
export function not<T extends unknown[]>(
|
||||
predicate: Predicate<T>
|
||||
): Predicate<T> {
|
||||
return (...args: T) => !predicate(...args);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue