diff --git a/docker/example.env b/docker/example.env index 52af38e91191..b0e3f94172a2 100644 --- a/docker/example.env +++ b/docker/example.env @@ -1,45 +1,44 @@ -#url of the frontend, this must be accessible by your clients browser +# Copy this file to `.env` before starting the containers + +### === Required Config === + +# URL of the frontend (accessible by browser) MONKEYTYPE_FRONTENDURL=http://myserver:8080 -#url of the backend server, this must be accessible by your clients browser +# URL of the backend (accessible by browser) MONKEYTYPE_BACKENDURL=http://myserver:5005 -# firebase config -# uncomment below config if you need user accounts -#FIREBASE_APIKEY= -#FIREBASE_AUTHDOMAIN= -#FIREBASE_PROJECTID= -#FIREBASE_STORAGEBUCKET= -#FIREBASE_MESSAGINGSENDERID= -#FIREBASE_APPID= - - -# email server config -# uncomment below if you want to send emails for e.g. password reset -#EMAIL_HOST=mail.myserver -#EMAIL_USER=mailuser -#EMAIL_PASS=mailpass -#EMAIL_PORT=465 -#EMAIL_FROM="Support " - -# google recaptcha -# uncomment below config if you need user accounts -# you can use these defaults if you host this privately -#RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI -#RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe +### === Optional: Google reCAPTCHA === + +# Default keys below work for localhost/private instances +# RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI +# RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe + RECAPTCHA_SITE_KEY= RECAPTCHA_SECRET= -# use alternative ports +### === Optional: Firebase === -# port of the frontend http server -# HTTP_PORT=8080 +# Uncomment if using user accounts +# FIREBASE_APIKEY=AIzaSy******** +# FIREBASE_AUTHDOMAIN=your-app.firebaseapp.com +# FIREBASE_PROJECTID=your-app +# FIREBASE_STORAGEBUCKET=your-app.appspot.com +# FIREBASE_MESSAGINGSENDERID=1234567890 +# FIREBASE_APPID=1:1234567890:web:abcdef123456 -# port of the backend api server -# BACKEND_PORT=5005 +### === Optional: Email Server === +# Enables email (e.g. password reset) -# port of the redis server, not exposed by default -# REDIS_PORT=6379 +# EMAIL_HOST=smtp.mailserver.com +# EMAIL_USER=your@email.com +# EMAIL_PASS=password +# EMAIL_PORT=465 +# EMAIL_FROM="Support " + +### === Optional: Custom Ports === -# port of the mongodb server, not exposed by default +# HTTP_PORT=8080 +# BACKEND_PORT=5005 +# REDIS_PORT=6379 # MONGO_PORT=27017 diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index c77a28756bd5..43f57ed6f777 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -256,9 +256,6 @@ step="1" value="" /> -
@@ -288,9 +285,6 @@ step="1" value="" /> -
@@ -322,9 +316,6 @@ step="1" value="" /> -
@@ -792,9 +783,6 @@ step="1" value="" /> -
@@ -1020,9 +1008,6 @@ min="10" max="90" /> -
@@ -1131,9 +1116,6 @@ class="input" min="0" /> - @@ -1154,9 +1136,6 @@ class="input" tabindex="0" /> - diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss index b69433b8aa93..db93648cc263 100644 --- a/frontend/src/styles/settings.scss +++ b/frontend/src/styles/settings.scss @@ -90,7 +90,7 @@ .inputAndButton { display: grid; grid-template-columns: auto min-content; - gap: 0.5rem; + // gap: 0.5rem; margin-bottom: 0.5rem; span { @@ -104,6 +104,20 @@ margin-right: 0rem; } } + + .hasError { + animation: shake 0.1s ease-in-out infinite; + } + + .statusIndicator { + opacity: 0; + } + &:has(input:focus), + &:has([data-indicator-status="failed"]) { + .statusIndicator { + opacity: 1; + } + } } .rangeGroup { diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index d05b1e229192..aa6f88f1883a 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -14,6 +14,7 @@ import { Command, CommandsSubgroup, CommandWithValidation } from "./types"; import { areSortedArraysEqual } from "../utils/arrays"; import { parseIntOptional } from "../utils/numbers"; import { debounce } from "throttle-debounce"; +import { createInputEventHandler } from "../elements/input-validation"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -618,6 +619,18 @@ async function runActiveCommand(): Promise { value: command.defaultValue?.() ?? "", icon: command.icon ?? "fa-chevron-right", }; + if ("validation" in command && !handlersCache.has(command.id)) { + const commandWithValidation = command as CommandWithValidation; + const handler = createInputEventHandler( + updateValidationResult, + commandWithValidation.validation, + "inputValueConvert" in commandWithValidation + ? commandWithValidation.inputValueConvert + : undefined + ); + handlersCache.set(command.id, handler); + } + await updateInput(inputModeParams.value as string); hideCommands(); } else if (command.subgroup) { @@ -788,48 +801,10 @@ function updateValidationResult( } } -async function isValid( - checkValue: unknown, - originalValue: string, - originalInput: HTMLInputElement, - validation: CommandWithValidation["validation"] -): Promise { - updateValidationResult({ status: "checking" }); - - if (validation.schema !== undefined) { - const schemaResult = validation.schema.safeParse(checkValue); - - if (!schemaResult.success) { - updateValidationResult({ - status: "failed", - errorMessage: schemaResult.error.errors - .map((err) => err.message) - .join(", "), - }); - return; - } - } - - if (validation.isValid === undefined) { - updateValidationResult({ status: "success" }); - return; - } - - const result = await validation.isValid(checkValue); - if (originalInput.value !== originalValue) { - //value has change in the meantime, discard result - return; - } - - if (result === true) { - updateValidationResult({ status: "success" }); - } else { - updateValidationResult({ - status: "failed", - errorMessage: result, - }); - } -} +/* + * Handlers needs to be created only once per command to ensure they debounce with the given delay + */ +const handlersCache = new Map Promise>(); const modal = new AnimatedModal({ dialogId: "commandLine", @@ -921,34 +896,24 @@ const modal = new AnimatedModal({ } }); - input.addEventListener( - "input", - debounce(100, async (e) => { - if ( - inputModeParams === null || - inputModeParams.command === null || - !("validation" in inputModeParams.command) - ) { - return; - } - - const originalInput = (e as InputEvent).target as HTMLInputElement; - const currentValue = originalInput.value; - let checkValue: unknown = currentValue; - const command = - inputModeParams.command as CommandWithValidation; + input.addEventListener("input", async (e) => { + if ( + inputModeParams === null || + inputModeParams.command === null || + !("validation" in inputModeParams.command) + ) { + return; + } - if ("inputValueConvert" in command) { - checkValue = command.inputValueConvert(currentValue); - } - await isValid( - checkValue, - currentValue, - originalInput, - command.validation + const handler = handlersCache.get(inputModeParams.command.id); + if (handler === undefined) { + throw new Error( + `Expected handler for command ${inputModeParams.command.id} is missing` ); - }) - ); + } + + await handler(e); + }); modalEl.addEventListener("mousemove", (_e) => { mouseMode = true; diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index ad1c7ecbf142..ed4de15d2fe7 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -1,6 +1,6 @@ import { Config } from "@monkeytype/schemas/configs"; import AnimatedModal from "../utils/animated-modal"; -import { z } from "zod"; +import { Validation } from "../elements/input-validation"; // this file is needed becauase otherwise it would produce a circular dependency @@ -47,21 +47,7 @@ export type CommandWithValidation = (T extends string * If the schema is defined it is always checked first. * Only if the schema validaton is passed or missing the `isValid` method is called. */ - validation: { - /** - * Zod schema to validate the input value against. - * The indicator will show the error messages from the schema. - */ - schema?: z.Schema; - /** - * Custom async validation method. - * This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations. - * @param value current input value - * @param thisPopup the current modal - * @returns true if the `value` is valid, an errorMessage as string if it is invalid. - */ - isValid?: (value: T) => Promise; - }; + validation: Validation; }; export type CommandsSubgroup = { diff --git a/frontend/src/ts/constants/languages.ts b/frontend/src/ts/constants/languages.ts index 2c47dd738313..04cb4af50114 100644 --- a/frontend/src/ts/constants/languages.ts +++ b/frontend/src/ts/constants/languages.ts @@ -39,6 +39,7 @@ export const LanguageGroups: Record = { ], arabic: ["arabic", "arabic_10k"], arabic_egypt: ["arabic_egypt", "arabic_egypt_1k"], + arabic_morocco: ["arabic_morocco"], italian: [ "italian", "italian_1k", @@ -340,6 +341,7 @@ export const LanguageGroups: Record = { "code_arduino", "code_systemverilog", "code_elixir", + "code_gleam", "code_zig", "code_gdscript", "code_gdscript_2", 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 d68d3f98108f..18c1624edef6 100644 --- a/frontend/src/ts/elements/account-settings/ape-key-table.ts +++ b/frontend/src/ts/elements/account-settings/ape-key-table.ts @@ -4,7 +4,7 @@ import Ape from "../../ape"; import { ApeKey, ApeKeys } from "@monkeytype/schemas/ape-keys"; import { format } from "date-fns/format"; import { SimpleModal, TextArea } from "../../utils/simple-modal"; - +import { isAuthenticated } from "../../firebase"; const editApeKey = new SimpleModal({ id: "editApeKey", title: "Edit Ape key", @@ -160,6 +160,8 @@ let apeKeys: ApeKeys | null = {}; const element = $("#pageAccountSettings .tab[data-tab='apeKeys']"); async function getData(): Promise { + if (!isAuthenticated()) return false; + showLoaderRow(); const response = await Ape.apeKeys.get(); diff --git a/frontend/src/ts/elements/input-indicator.ts b/frontend/src/ts/elements/input-indicator.ts index 6bdca8eb2af2..ab32e754a51d 100644 --- a/frontend/src/ts/elements/input-indicator.ts +++ b/frontend/src/ts/elements/input-indicator.ts @@ -74,6 +74,7 @@ export class InputIndicator { } $(this.inputElement).css("padding-right", "2.1em"); + this.parentElement.attr("data-indicator-status", optionId); } get(): keyof typeof this.options | null { diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts new file mode 100644 index 000000000000..db71dde3e1d2 --- /dev/null +++ b/frontend/src/ts/elements/input-validation.ts @@ -0,0 +1,252 @@ +import { debounce } from "throttle-debounce"; +import { z, ZodType } from "zod"; +import { InputIndicator } from "./input-indicator"; +import { + ConfigKey, + ConfigSchema, + Config as ConfigType, +} from "@monkeytype/schemas/configs"; +import Config, * as UpdateConfig from "../config"; +import * as Notifications from "../elements/notifications"; + +export type ValidationResult = { + status: "checking" | "success" | "failed"; + errorMessage?: string; +}; + +export type Validation = { + /** + * Zod schema to validate the input value against. + * The indicator will show the error messages from the schema. + */ + schema?: z.Schema; + + /** + * Custom async validation method. + * This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations. + * @param value current input value + * @param thisPopup the current modal + * @returns true if the `value` is valid, an errorMessage as string if it is invalid. + */ + isValid?: (value: T) => Promise; + + /** custom debounce delay for `isValid` call. defaults to 100 */ + debounceDelay?: number; +}; +/** + * Create input handler for validated input element. + * the `callback` is called for each validation state change, including "checking". + * @param callback callback to call for each change of the validation status + * @param validation validation options + * @param inputValueConvert convert method from string to the schema type, mandatory if the schema is not a string schema + * @returns debounced input event handler + */ +export function createInputEventHandler( + callback: (result: ValidationResult) => void, + validation: Validation, + inputValueConvert?: (val: string) => T +): (e: Event) => Promise { + let callIsValid = + validation.isValid !== undefined + ? debounce( + validation.debounceDelay ?? 100, + async ( + originalInput: HTMLInputElement, + currentValue: string, + checkValue: T + ) => { + const result = await validation.isValid?.(checkValue); + if (originalInput.value !== currentValue) { + //value has change in the meantime, discard result + return; + } + + if (result === true) { + callback({ status: "success" }); + } else { + callback({ status: "failed", errorMessage: result }); + } + } + ) + : undefined; + + return async (e) => { + const originalInput = e.target as HTMLInputElement; + const currentValue = originalInput.value; + let checkValue: unknown = currentValue; + + if (inputValueConvert !== undefined) { + checkValue = inputValueConvert(currentValue); + } + + callback({ status: "checking" }); + + if (validation.schema !== undefined) { + const schemaResult = validation.schema.safeParse(checkValue); + + if (!schemaResult.success) { + callback({ + status: "failed", + errorMessage: schemaResult.error.errors + .map((err) => err.message) + .join(", "), + }); + return; + } + } + + if (callIsValid === undefined) { + callback({ status: "success" }); + //call original handler if defined + originalInput.oninput?.(e); + return; + } + + callIsValid(originalInput, currentValue, checkValue as T); + //call original handler if defined + originalInput.oninput?.(e); + }; +} + +export type ValidationOptions = (T extends string + ? Validation + : Validation & { + /** convert string input. For `number`s use `Number` constructor */ + inputValueConvert: (val: string) => T; + }) & { + /** optional callback is called for each change of the validation result */ + callback?: (result: ValidationResult) => void; +}; + +/** + * adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation + * @param inputElement + * @param options + */ +export function validateWithIndicator( + inputElement: HTMLInputElement, + options: ValidationOptions +): void { + //use indicator + const indicator = new InputIndicator(inputElement, { + success: { + icon: "fa-check", + level: 1, + }, + failed: { + icon: "fa-times", + level: -1, + }, + checking: { + icon: "fa-circle-notch", + spinIcon: true, + level: 0, + }, + }); + const callback = (result: ValidationResult): void => { + if (result.status === "failed") { + indicator.show(result.status, result.errorMessage); + } else { + indicator.show(result.status); + } + options.callback?.(result); + }; + + const handler = createInputEventHandler( + callback, + options, + "inputValueConvert" in options ? options.inputValueConvert : undefined + ); + + inputElement.addEventListener("input", handler); +} + +export type ConfigInputOptions = { + input: HTMLInputElement | null; + configName: K; + validation?: (T extends string + ? Omit, "schema"> + : Omit, "schema"> & { + inputValueConvert: (val: string) => T; + }) & { + /**set to `true` to validate against the `ConfigSchema` */ + schema: boolean; + /** optional callback is called for each change of the validation result */ + validationCallback?: (result: ValidationResult) => void; + }; +}; + +/** + * Adds input event listeners to the given input element. On `focusOut` and when pressing `Enter` the current value is stored in the Config using `genericSet`. + * Note: Config is not updated if the value has not changed. + * + * If validation is set, Adds input validation using `InputIndicator` to the given input element. Config is only updated if the value is valid. + * + */ +export function handleConfigInput({ + input, + configName, + validation, +}: ConfigInputOptions): void { + if (input === null) { + throw new Error(`Failed to find input element for ${configName}`); + } + + const inputValueConvert = + validation !== undefined && "inputValueConvert" in validation + ? validation.inputValueConvert + : undefined; + let status: ValidationResult["status"] = "checking"; + + if (validation !== undefined) { + const schema = ConfigSchema.shape[configName] as ZodType; + + validateWithIndicator(input, { + schema: validation.schema ? schema : undefined, + //@ts-expect-error this is fine + isValid: validation.isValid, + inputValueConvert, + callback: (result) => { + status = result.status; + }, + }); + } + + const handleStore = (): void => { + if (input.value === "") { + //use last config value, clear validation + input.value = new String(Config[configName]).toString(); + input.dispatchEvent(new Event("input")); + } + if (status === "failed") { + const parent = $(input.parentElement as HTMLElement); + parent + .stop(true, true) + .addClass("hasError") + .animate({ undefined: 1 }, 500, () => { + parent.removeClass("hasError"); + }); + return; + } + const value = (inputValueConvert?.(input.value) ?? + input.value) as ConfigType[T]; + + if (Config[configName] === value) { + return; + } + const didConfigSave = UpdateConfig.genericSet(configName, value, false); + + if (didConfigSave) { + Notifications.add("Saved", 1, { + duration: 1, + }); + } + }; + + input.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + handleStore(); + } + }); + input.addEventListener("focusout", (e) => handleStore()); +} diff --git a/frontend/src/ts/elements/settings/settings-group.ts b/frontend/src/ts/elements/settings/settings-group.ts index e093626a279f..c590bda6dec7 100644 --- a/frontend/src/ts/elements/settings/settings-group.ts +++ b/frontend/src/ts/elements/settings/settings-group.ts @@ -1,31 +1,68 @@ -import { ConfigValue } from "@monkeytype/schemas/configs"; +import { Config as ConfigType, ConfigKey } from "@monkeytype/schemas/configs"; + import Config from "../../config"; import * as Notifications from "../notifications"; import SlimSelect from "slim-select"; import { debounce } from "throttle-debounce"; +import { + handleConfigInput, + ConfigInputOptions, + Validation, +} from "../input-validation"; + +type Mode = "select" | "button" | "range" | "input"; -type Mode = "select" | "button" | "range"; +export type SimpleValidation = Omit, "schema"> & { + schema?: true; +}; -export default class SettingsGroup { - public configName: string; +export default class SettingsGroup { + public configName: K; public configFunction: (param: T, nosave?: boolean) => boolean; public mode: Mode; public setCallback?: () => void; public updateCallback?: () => void; private elements: Element[]; + private validation?: T extends string + ? SimpleValidation + : SimpleValidation & { + inputValueConvert: (val: string) => T; + }; constructor( - configName: string, + configName: K, configFunction: (param: T, nosave?: boolean) => boolean, mode: Mode, - setCallback?: () => void, - updateCallback?: () => void + options?: { + setCallback?: () => void; + updateCallback?: () => void; + validation?: T extends string + ? SimpleValidation + : SimpleValidation & { + inputValueConvert: (val: string) => T; + }; + } ) { this.configName = configName; this.mode = mode; this.configFunction = configFunction; - this.setCallback = setCallback; - this.updateCallback = updateCallback; + this.setCallback = options?.setCallback; + this.updateCallback = options?.updateCallback; + this.validation = options?.validation; + + const convertValue = (value: string): T => { + let typed = value as T; + if ( + this.validation !== undefined && + "inputValueConvert" in this.validation + ) { + typed = this.validation.inputValueConvert(value); + } + if (typed === "true") typed = true as T; + if (typed === "false") typed = false as T; + + return typed; + }; if (this.mode === "select") { const el = document.querySelector( @@ -68,8 +105,9 @@ export default class SettingsGroup { ) { return; } - const value = button.getAttribute("data-config-value"); - if (value === undefined || value === "") { + + let value = button.getAttribute("data-config-value"); + if (value === null || value === "") { console.error( `Failed to handle settings button click for ${configName}: data-${configName} is missing or empty.` ); @@ -79,14 +117,39 @@ export default class SettingsGroup { ); return; } - let typed = value as T; - if (typed === "true") typed = true as T; - if (typed === "false") typed = false as T; + + let typed = convertValue(value); this.setValue(typed); }); } this.elements = Array.from(els); + } else if (this.mode === "input") { + const input: HTMLInputElement | null = document.querySelector(` + .pageSettings .section[data-config-name=${this.configName}] .inputs .inputAndButton input`); + if (input === null) { + throw new Error(`Failed to find input element for ${configName}`); + } + + let validation; + if (this.validation !== undefined) { + validation = { + schema: this.validation.schema ?? false, + isValid: this.validation.isValid, + inputValueConvert: + "inputValueConvert" in this.validation + ? this.validation.inputValueConvert + : undefined, + }; + } + + handleConfigInput({ + input, + configName: this.configName, + validation, + } as ConfigInputOptions); + + this.elements = [input]; } else if (this.mode === "range") { const el = document.querySelector( `.pageSettings .section[data-config-name=${this.configName}] input[type=range]` @@ -126,18 +189,18 @@ export default class SettingsGroup { this.updateUI(); } - setValue(value: T): void { - if (Config[this.configName as keyof typeof Config] === value) { - return; + setValue(value: T): boolean { + if (Config[this.configName] === value) { + return false; } - this.configFunction(value); + const didSet = this.configFunction(value); this.updateUI(); if (this.setCallback) this.setCallback(); + return didSet; } updateUI(valueOverride?: T): void { - const newValue = - valueOverride ?? (Config[this.configName as keyof typeof Config] as T); + const newValue = valueOverride ?? (Config[this.configName] as T); if (this.mode === "select") { const select = this.elements?.[0] as HTMLSelectElement | null | undefined; diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index 30a36ea4549c..df8442f9c0de 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -478,6 +478,7 @@ list.updateName = new SimpleModal({ return checkNameResponse === 200 ? true : "Name not available"; }, + debounceDelay: 1000, }, }, ], diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 260b18c7b619..1fc9eed6056b 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -20,12 +20,12 @@ 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, ConfigKeySchema, + ConfigKey, } from "@monkeytype/schemas/configs"; import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox"; import { getActiveFunboxNames } from "../test/funbox/list"; @@ -39,14 +39,15 @@ import { LayoutName } from "@monkeytype/schemas/layouts"; import { LanguageGroupNames, LanguageGroups } from "../constants/languages"; import { Language } from "@monkeytype/schemas/languages"; import { z } from "zod"; +import { handleConfigInput } from "../elements/input-validation"; let settingsInitialized = false; -type SettingsGroups = Record>; +type SettingsGroups = Partial<{ [K in ConfigKey]: SettingsGroup }>; let customLayoutFluidSelect: SlimSelect | undefined; let customPolyglotSelect: SlimSelect | undefined; -export const groups: SettingsGroups = {}; +export const groups: SettingsGroups = {}; const HighlightSchema = ConfigKeySchema.or( z.enum([ @@ -71,396 +72,407 @@ async function initGroups(): Promise { "smoothCaret", UpdateConfig.setSmoothCaret, "button" - ) as SettingsGroup; + ); groups["codeUnindentOnBackspace"] = new SettingsGroup( "codeUnindentOnBackspace", UpdateConfig.setCodeUnindentOnBackspace, "button" - ) as SettingsGroup; + ); groups["difficulty"] = new SettingsGroup( "difficulty", UpdateConfig.setDifficulty, "button" - ) as SettingsGroup; + ); groups["quickRestart"] = new SettingsGroup( "quickRestart", UpdateConfig.setQuickRestartMode, "button" - ) as SettingsGroup; + ); groups["showAverage"] = new SettingsGroup( "showAverage", UpdateConfig.setShowAverage, "button" - ) as SettingsGroup; + ); groups["keymapMode"] = new SettingsGroup( "keymapMode", UpdateConfig.setKeymapMode, "button", - () => { - groups["showLiveWpm"]?.updateUI(); - }, - () => { - if (Config.keymapMode === "off") { - $(".pageSettings .section[data-config-name='keymapStyle']").addClass( - "hidden" - ); - $(".pageSettings .section[data-config-name='keymapLayout']").addClass( - "hidden" - ); - $( - ".pageSettings .section[data-config-name='keymapLegendStyle']" - ).addClass("hidden"); - $( - ".pageSettings .section[data-config-name='keymapShowTopRow']" - ).addClass("hidden"); - $(".pageSettings .section[data-config-name='keymapSize']").addClass( - "hidden" - ); - } else { - $(".pageSettings .section[data-config-name='keymapStyle']").removeClass( - "hidden" - ); - $( - ".pageSettings .section[data-config-name='keymapLayout']" - ).removeClass("hidden"); - $( - ".pageSettings .section[data-config-name='keymapLegendStyle']" - ).removeClass("hidden"); - $( - ".pageSettings .section[data-config-name='keymapShowTopRow']" - ).removeClass("hidden"); - $(".pageSettings .section[data-config-name='keymapSize']").removeClass( - "hidden" - ); - } + { + updateCallback: () => { + if (Config.keymapMode === "off") { + $(".pageSettings .section[data-config-name='keymapStyle']").addClass( + "hidden" + ); + $(".pageSettings .section[data-config-name='keymapLayout']").addClass( + "hidden" + ); + $( + ".pageSettings .section[data-config-name='keymapLegendStyle']" + ).addClass("hidden"); + $( + ".pageSettings .section[data-config-name='keymapShowTopRow']" + ).addClass("hidden"); + $(".pageSettings .section[data-config-name='keymapSize']").addClass( + "hidden" + ); + } else { + $( + ".pageSettings .section[data-config-name='keymapStyle']" + ).removeClass("hidden"); + $( + ".pageSettings .section[data-config-name='keymapLayout']" + ).removeClass("hidden"); + $( + ".pageSettings .section[data-config-name='keymapLegendStyle']" + ).removeClass("hidden"); + $( + ".pageSettings .section[data-config-name='keymapShowTopRow']" + ).removeClass("hidden"); + $( + ".pageSettings .section[data-config-name='keymapSize']" + ).removeClass("hidden"); + } + }, } - ) as SettingsGroup; - groups["keymapMatrix"] = new SettingsGroup( + ); + groups["keymapStyle"] = new SettingsGroup( "keymapStyle", UpdateConfig.setKeymapStyle, "button" - ) as SettingsGroup; + ); groups["keymapLayout"] = new SettingsGroup( "keymapLayout", UpdateConfig.setKeymapLayout, "select" - ) as SettingsGroup; + ); groups["keymapLegendStyle"] = new SettingsGroup( "keymapLegendStyle", UpdateConfig.setKeymapLegendStyle, "button" - ) as SettingsGroup; + ); groups["keymapShowTopRow"] = new SettingsGroup( "keymapShowTopRow", UpdateConfig.setKeymapShowTopRow, "button" - ) as SettingsGroup; + ); groups["keymapSize"] = new SettingsGroup( "keymapSize", UpdateConfig.setKeymapSize, "range" - ) as SettingsGroup; + ); groups["showKeyTips"] = new SettingsGroup( "showKeyTips", UpdateConfig.setKeyTips, "button" - ) as SettingsGroup; + ); groups["freedomMode"] = new SettingsGroup( "freedomMode", UpdateConfig.setFreedomMode, "button", - () => { - groups["confidenceMode"]?.updateUI(); + { + setCallback: () => { + groups["confidenceMode"]?.updateUI(); + }, } - ) as SettingsGroup; + ); groups["strictSpace"] = new SettingsGroup( "strictSpace", UpdateConfig.setStrictSpace, "button" - ) as SettingsGroup; + ); groups["oppositeShiftMode"] = new SettingsGroup( "oppositeShiftMode", UpdateConfig.setOppositeShiftMode, "button" - ) as SettingsGroup; + ); groups["confidenceMode"] = new SettingsGroup( "confidenceMode", UpdateConfig.setConfidenceMode, "button", - () => { - groups["freedomMode"]?.updateUI(); - groups["stopOnError"]?.updateUI(); + { + setCallback: () => { + groups["freedomMode"]?.updateUI(); + groups["stopOnError"]?.updateUI(); + }, } - ) as SettingsGroup; + ); groups["indicateTypos"] = new SettingsGroup( "indicateTypos", UpdateConfig.setIndicateTypos, "button" - ) as SettingsGroup; + ); groups["hideExtraLetters"] = new SettingsGroup( "hideExtraLetters", UpdateConfig.setHideExtraLetters, "button" - ) as SettingsGroup; + ); groups["blindMode"] = new SettingsGroup( "blindMode", UpdateConfig.setBlindMode, "button" - ) as SettingsGroup; + ); groups["quickEnd"] = new SettingsGroup( "quickEnd", UpdateConfig.setQuickEnd, "button" - ) as SettingsGroup; + ); groups["repeatQuotes"] = new SettingsGroup( "repeatQuotes", UpdateConfig.setRepeatQuotes, "button" - ) as SettingsGroup; - groups["ads"] = new SettingsGroup( - "ads", - UpdateConfig.setAds, - "button" - ) as SettingsGroup; + ); + groups["ads"] = new SettingsGroup("ads", UpdateConfig.setAds, "button"); groups["alwaysShowWordsHistory"] = new SettingsGroup( "alwaysShowWordsHistory", UpdateConfig.setAlwaysShowWordsHistory, "button" - ) as SettingsGroup; + ); groups["britishEnglish"] = new SettingsGroup( "britishEnglish", UpdateConfig.setBritishEnglish, "button" - ) as SettingsGroup; + ); groups["singleListCommandLine"] = new SettingsGroup( "singleListCommandLine", UpdateConfig.setSingleListCommandLine, "button" - ) as SettingsGroup; + ); groups["capsLockWarning"] = new SettingsGroup( "capsLockWarning", UpdateConfig.setCapsLockWarning, "button" - ) as SettingsGroup; + ); groups["flipTestColors"] = new SettingsGroup( "flipTestColors", UpdateConfig.setFlipTestColors, "button" - ) as SettingsGroup; + ); groups["showOutOfFocusWarning"] = new SettingsGroup( "showOutOfFocusWarning", UpdateConfig.setShowOutOfFocusWarning, "button" - ) as SettingsGroup; + ); groups["colorfulMode"] = new SettingsGroup( "colorfulMode", UpdateConfig.setColorfulMode, "button" - ) as SettingsGroup; + ); groups["startGraphsAtZero"] = new SettingsGroup( "startGraphsAtZero", UpdateConfig.setStartGraphsAtZero, "button" - ) as SettingsGroup; + ); groups["autoSwitchTheme"] = new SettingsGroup( "autoSwitchTheme", UpdateConfig.setAutoSwitchTheme, "button" - ) as SettingsGroup; + ); groups["randomTheme"] = new SettingsGroup( "randomTheme", UpdateConfig.setRandomTheme, "button" - ) as SettingsGroup; + ); groups["stopOnError"] = new SettingsGroup( "stopOnError", UpdateConfig.setStopOnError, "button", - () => { - groups["confidenceMode"]?.updateUI(); + { + setCallback: () => { + groups["confidenceMode"]?.updateUI(); + }, } - ) as SettingsGroup; + ); groups["soundVolume"] = new SettingsGroup( "soundVolume", UpdateConfig.setSoundVolume, "range" - ) as SettingsGroup; + ); groups["playTimeWarning"] = new SettingsGroup( "playTimeWarning", UpdateConfig.setPlayTimeWarning, "button", - () => { - if (Config.playTimeWarning !== "off") void Sound.playTimeWarning(); + { + setCallback: () => { + if (Config.playTimeWarning !== "off") void Sound.playTimeWarning(); + }, } - ) as SettingsGroup; + ); groups["playSoundOnError"] = new SettingsGroup( "playSoundOnError", UpdateConfig.setPlaySoundOnError, "button", - () => { - if (Config.playSoundOnError !== "off") void Sound.playError(); + { + setCallback: () => { + if (Config.playSoundOnError !== "off") void Sound.playError(); + }, } - ) as SettingsGroup; + ); groups["playSoundOnClick"] = new SettingsGroup( "playSoundOnClick", UpdateConfig.setPlaySoundOnClick, "button", - () => { - if (Config.playSoundOnClick !== "off") void Sound.playClick("KeyQ"); + { + setCallback: () => { + if (Config.playSoundOnClick !== "off") void Sound.playClick("KeyQ"); + }, } - ) as SettingsGroup; + ); groups["showAllLines"] = new SettingsGroup( "showAllLines", UpdateConfig.setShowAllLines, "button" - ) as SettingsGroup; + ); groups["paceCaret"] = new SettingsGroup( "paceCaret", UpdateConfig.setPaceCaret, "button" - ) as SettingsGroup; + ); groups["repeatedPace"] = new SettingsGroup( "repeatedPace", UpdateConfig.setRepeatedPace, "button" - ) as SettingsGroup; + ); groups["minWpm"] = new SettingsGroup( "minWpm", UpdateConfig.setMinWpm, "button" - ) as SettingsGroup; + ); groups["minAcc"] = new SettingsGroup( "minAcc", UpdateConfig.setMinAcc, "button" - ) as SettingsGroup; + ); groups["minBurst"] = new SettingsGroup( "minBurst", UpdateConfig.setMinBurst, "button" - ) as SettingsGroup; + ); groups["smoothLineScroll"] = new SettingsGroup( "smoothLineScroll", UpdateConfig.setSmoothLineScroll, "button" - ) as SettingsGroup; + ); groups["lazyMode"] = new SettingsGroup( "lazyMode", UpdateConfig.setLazyMode, "button" - ) as SettingsGroup; + ); groups["layout"] = new SettingsGroup( "layout", UpdateConfig.setLayout, "select" - ) as SettingsGroup; + ); groups["language"] = new SettingsGroup( "language", UpdateConfig.setLanguage, "select" - ) as SettingsGroup; + ); groups["fontSize"] = new SettingsGroup( "fontSize", UpdateConfig.setFontSize, - "button" - ) as SettingsGroup; + "input", + { validation: { schema: true, inputValueConvert: Number } } + ); groups["maxLineWidth"] = new SettingsGroup( "maxLineWidth", UpdateConfig.setMaxLineWidth, - "button" - ) as SettingsGroup; + "input", + { validation: { schema: true, inputValueConvert: Number } } + ); groups["caretStyle"] = new SettingsGroup( "caretStyle", UpdateConfig.setCaretStyle, "button" - ) as SettingsGroup; + ); groups["paceCaretStyle"] = new SettingsGroup( "paceCaretStyle", UpdateConfig.setPaceCaretStyle, "button" - ) as SettingsGroup; + ); groups["timerStyle"] = new SettingsGroup( "timerStyle", UpdateConfig.setTimerStyle, "button" - ) as SettingsGroup; + ); groups["liveSpeedStyle"] = new SettingsGroup( "liveSpeedStyle", UpdateConfig.setLiveSpeedStyle, "button" - ) as SettingsGroup; + ); groups["liveAccStyle"] = new SettingsGroup( "liveAccStyle", UpdateConfig.setLiveAccStyle, "button" - ) as SettingsGroup; + ); groups["liveBurstStyle"] = new SettingsGroup( "liveBurstStyle", UpdateConfig.setLiveBurstStyle, "button" - ) as SettingsGroup; + ); groups["highlightMode"] = new SettingsGroup( "highlightMode", UpdateConfig.setHighlightMode, "button" - ) as SettingsGroup; + ); groups["tapeMode"] = new SettingsGroup( "tapeMode", UpdateConfig.setTapeMode, "button" - ) as SettingsGroup; + ); groups["tapeMargin"] = new SettingsGroup( "tapeMargin", UpdateConfig.setTapeMargin, - "button" - ) as SettingsGroup; + "input", + { validation: { schema: true, inputValueConvert: Number } } + ); groups["timerOpacity"] = new SettingsGroup( "timerOpacity", UpdateConfig.setTimerOpacity, "button" - ) as SettingsGroup; + ); groups["timerColor"] = new SettingsGroup( "timerColor", UpdateConfig.setTimerColor, "button" - ) as SettingsGroup; + ); groups["fontFamily"] = new SettingsGroup( "fontFamily", UpdateConfig.setFontFamily, "button", - undefined, - () => { - const customButton = $( - ".pageSettings .section[data-config-name='fontFamily'] .buttons button[data-config-value='custom']" - ); + { + updateCallback: () => { + const customButton = $( + ".pageSettings .section[data-config-name='fontFamily'] .buttons button[data-config-value='custom']" + ); - if ( - $( - ".pageSettings .section[data-config-name='fontFamily'] .buttons .active" - ).length === 0 - ) { - customButton.addClass("active"); - customButton.text(`Custom (${Config.fontFamily.replace(/_/g, " ")})`); - } else { - customButton.text("Custom"); - } + if ( + $( + ".pageSettings .section[data-config-name='fontFamily'] .buttons .active" + ).length === 0 + ) { + customButton.addClass("active"); + customButton.text(`Custom (${Config.fontFamily.replace(/_/g, " ")})`); + } else { + customButton.text("Custom"); + } + }, } - ) as SettingsGroup; + ); groups["alwaysShowDecimalPlaces"] = new SettingsGroup( "alwaysShowDecimalPlaces", UpdateConfig.setAlwaysShowDecimalPlaces, "button" - ) as SettingsGroup; + ); groups["typingSpeedUnit"] = new SettingsGroup( "typingSpeedUnit", UpdateConfig.setTypingSpeedUnit, "button" - ) as SettingsGroup; + ); groups["customBackgroundSize"] = new SettingsGroup( "customBackgroundSize", UpdateConfig.setCustomBackgroundSize, "button" - ) as SettingsGroup; + ); } async function fillSettingsPage(): Promise { @@ -641,6 +653,55 @@ async function fillSettingsPage(): Promise { }, }); + handleConfigInput({ + input: document.querySelector( + ".pageSettings .section[data-config-name='minWpm'] input" + ), + configName: "minWpmCustomSpeed", + validation: { + schema: true, + inputValueConvert: (it) => + getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( + new Number(it).valueOf() + ), + }, + }); + + handleConfigInput({ + input: document.querySelector( + ".pageSettings .section[data-config-name='minAcc'] input" + ), + configName: "minAccCustom", + validation: { + schema: true, + inputValueConvert: Number, + }, + }); + + handleConfigInput({ + input: document.querySelector( + ".pageSettings .section[data-config-name='minBurst'] input" + ), + configName: "minBurstCustomSpeed", + validation: { + schema: true, + inputValueConvert: (it) => + getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( + new Number(it).valueOf() + ), + }, + }); + + handleConfigInput({ + input: document.querySelector( + ".pageSettings .section[data-config-name='paceCaret'] input" + ), + configName: "paceCaretCustomSpeed", + validation: { + schema: true, + inputValueConvert: Number, + }, + }); setEventDisabled(true); await initGroups(); @@ -749,15 +810,21 @@ function refreshPresetsSettingsSection(): void { } } -export async function update(): Promise { +export async function update( + options: { + eventKey?: ConfigEvent.ConfigEventKey; + } = {} +): Promise { if (Config.showKeyTips) { $(".pageSettings .tip").removeClass("hidden"); } else { $(".pageSettings .tip").addClass("hidden"); } - for (const group of Object.keys(groups)) { - groups[group]?.updateUI(); + for (const group of Object.values(groups)) { + if ("updateUI" in group) { + group.updateUI(); + } } refreshTagsSettingsSection(); @@ -769,25 +836,45 @@ export async function update(): Promise { ThemePicker.setCustomInputs(true); // ThemePicker.updateActiveButton(); - $( - ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed" - ).val( + const setInputValue = ( + key: ConfigKey, + query: string, + value: string | number + ): void => { + if (options.eventKey === undefined || options.eventKey === key) { + const element = document.querySelector(query) as HTMLInputElement; + if (element === null) { + throw new Error("Unknown input element " + query); + } + + element.value = new String(value).toString(); + element.dispatchEvent(new Event("input")); + } + }; + + setInputValue( + "paceCaret", + ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed", getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm( Config.paceCaretCustomSpeed ) ); - $( - ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed" - ).val( + setInputValue( + "minWpmCustomSpeed", + ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed", getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(Config.minWpmCustomSpeed) ); - $(".pageSettings .section[data-config-name='minAcc'] input.customMinAcc").val( + + setInputValue( + "minAccCustom", + ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc", Config.minAccCustom ); - $( - ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst" - ).val( + + setInputValue( + "minBurstCustomSpeed", + ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst", getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm( Config.minBurstCustomSpeed ) @@ -814,25 +901,35 @@ export async function update(): Promise { } updateCustomBackgroundRemoveButtonVisibility(); - $(".pageSettings .section[data-config-name='fontSize'] input").val( + setInputValue( + "fontSize", + ".pageSettings .section[data-config-name='fontSize'] input", Config.fontSize ); - $(".pageSettings .section[data-config-name='maxLineWidth'] input").val( + setInputValue( + "maxLineWidth", + ".pageSettings .section[data-config-name='maxLineWidth'] input", Config.maxLineWidth ); - $(".pageSettings .section[data-config-name='keymapSize'] input").val( + setInputValue( + "keymapSize", + ".pageSettings .section[data-config-name='keymapSize'] input", Config.keymapSize ); - $(".pageSettings .section[data-config-name='tapeMargin'] input").val( + setInputValue( + "tapeMargin", + ".pageSettings .section[data-config-name='tapeMargin'] input", Config.tapeMargin ); - $( - ".pageSettings .section[data-config-name='customBackgroundSize'] input" - ).val(Config.customBackground); + setInputValue( + "customBackground", + ".pageSettings .section[data-config-name='customBackgroundSize'] input", + Config.customBackground + ); if (isAuthenticated()) { showAccountSection(); @@ -907,130 +1004,6 @@ function updateCustomBackgroundRemoveButtonVisibility(): void { } } -$(".pageSettings .section[data-config-name='paceCaret']").on( - "focusout", - "input.customPaceCaretSpeed", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setPaceCaretCustomSpeed(newConfigValue); - } -); - -$(".pageSettings .section[data-config-name='paceCaret']").on( - "click", - "button.save", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setPaceCaretCustomSpeed(newConfigValue); - } -); - -$(".pageSettings .section[data-config-name='minWpm']").on( - "focusout", - "input.customMinWpmSpeed", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setMinWpmCustomSpeed(newConfigValue); - } -); - -$(".pageSettings .section[data-config-name='minWpm']").on( - "click", - "button.save", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setMinWpmCustomSpeed(newConfigValue); - } -); - -$(".pageSettings .section[data-config-name='minAcc']").on( - "focusout", - "input.customMinAcc", - () => { - UpdateConfig.setMinAccCustom( - parseInt( - $( - ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc" - ).val() as string - ) - ); - } -); - -$(".pageSettings .section[data-config-name='minAcc']").on( - "click", - "button.save", - () => { - UpdateConfig.setMinAccCustom( - parseInt( - $( - ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc" - ).val() as string - ) - ); - } -); - -$(".pageSettings .section[data-config-name='minBurst']").on( - "focusout", - "input.customMinBurst", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setMinBurstCustomSpeed(newConfigValue); - } -); - -$(".pageSettings .section[data-config-name='minBurst']").on( - "click", - "button.save", - () => { - const inputValue = parseInt( - $( - ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst" - ).val() as string - ); - const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm( - inputValue - ); - UpdateConfig.setMinBurstCustomSpeed(newConfigValue); - } -); - //funbox $(".pageSettings .section[data-config-name='funbox'] .buttons").on( "click", @@ -1133,131 +1106,6 @@ $( } }); -$( - ".pageSettings .section[data-config-name='fontSize'] .inputAndButton button.save" -).on("click", () => { - const didConfigSave = UpdateConfig.setFontSize( - parseFloat( - $( - ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } -}); - -$( - ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input" -).on("keypress", (e) => { - if (e.key === "Enter") { - const didConfigSave = UpdateConfig.setFontSize( - parseFloat( - $( - ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } - } -}); - -$( - ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton button.save" -).on("click", () => { - const didConfigSave = UpdateConfig.setTapeMargin( - parseFloat( - $( - ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } -}); - -$( - ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" -).on("keypress", (e) => { - if (e.key === "Enter") { - const didConfigSave = UpdateConfig.setTapeMargin( - parseFloat( - $( - ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } - } -}); - -$( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton button.save" -).on("click", () => { - const didConfigSave = UpdateConfig.setMaxLineWidth( - parseFloat( - $( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } -}); - -$( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input" -).on("focusout", () => { - const didConfigSave = UpdateConfig.setMaxLineWidth( - parseFloat( - $( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } -}); - -$( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input" -).on("keypress", (e) => { - if (e.key === "Enter") { - const didConfigSave = UpdateConfig.setMaxLineWidth( - parseFloat( - $( - ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input" - ).val() as string - ) - ); - if (didConfigSave) { - Notifications.add("Saved", 1, { - duration: 1, - }); - } - } -}); - $( ".pageSettings .section[data-config-name='keymapSize'] .inputAndButton button.save" ).on("click", () => { @@ -1421,7 +1269,7 @@ ConfigEvent.subscribe((eventKey, eventValue) => { //make sure the page doesnt update a billion times when applying a preset/config at once if (configEventDisabled || eventKey === "saveToLocalStorage") return; if (ActivePage.get() === "settings" && eventKey !== "theme") { - void update(); + void update({ eventKey }); } }); diff --git a/frontend/src/ts/utils/simple-modal.ts b/frontend/src/ts/utils/simple-modal.ts index 0679e75bc60f..bd5200ba7a1b 100644 --- a/frontend/src/ts/utils/simple-modal.ts +++ b/frontend/src/ts/utils/simple-modal.ts @@ -4,8 +4,12 @@ import { format as dateFormat } from "date-fns/format"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import * as ConnectionState from "../states/connection"; -import { InputIndicator } from "../elements/input-indicator"; -import { debounce } from "throttle-debounce"; +import { + Validation, + ValidationOptions, + ValidationResult, + validateWithIndicator as withValidation, +} from "../elements/input-validation"; type CommonInput = { type: TType; @@ -21,12 +25,7 @@ type CommonInput = { * If the schema is defined it is always checked first. * Only if the schema validaton is passed or missing the `isValid` method is called. */ - validation?: { - /** - * Zod schema to validate the input value against. - * The indicator will show the error messages from the schema. - */ - schema?: Zod.Schema; + validation?: Omit, "isValid"> & { /** * Custom async validation method. * This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations. @@ -90,7 +89,7 @@ export type ExecReturn = { }; type FormInput = CommonInputType & { - indicator?: InputIndicator; + hasError?: boolean; currentValue: () => string; }; type SimpleModalOptions = { @@ -327,70 +326,23 @@ export class SimpleModal { }; if (input.validation !== undefined) { - const indicator = new InputIndicator(element, { - valid: { - icon: "fa-check", - level: 1, - }, - invalid: { - icon: "fa-times", - level: -1, + const options: ValidationOptions = { + schema: input.validation.schema ?? undefined, + isValid: + input.validation.isValid !== undefined + ? async (val: string) => { + //@ts-expect-error this is fine + return input.validation.isValid(val, this); + } + : undefined, + + callback: (result: ValidationResult) => { + input.hasError = result.status !== "success"; }, - checking: { - icon: "fa-circle-notch", - spinIcon: true, - level: 0, - }, - }); - input.indicator = indicator; - - const debouceIsValid = debounce(1000, async (value: string) => { - const result = await input.validation?.isValid?.(value, this); - - if (element.value !== value) { - //value of the input has changed in the meantime. discard - return; - } - - if (result === true) { - indicator.show("valid"); - } else { - indicator.show("invalid", result); - } - }); - - const validateInput = async (value: string): Promise => { - if (value === undefined || value === "") { - indicator.hide(); - return; - } - if (input.validation?.schema !== undefined) { - const schemaResult = input.validation.schema.safeParse(value); - if (!schemaResult.success) { - indicator.show( - "invalid", - schemaResult.error.errors.map((err) => err.message).join(", ") - ); - return; - } - } - - if (input.validation?.isValid !== undefined) { - indicator.show("checking"); - debouceIsValid(value); - return; - } - - indicator.show("valid"); + debounceDelay: input.validation.debounceDelay, }; - element.oninput = async (event) => { - const value = (event.target as HTMLInputElement).value; - await validateInput(value); - - //call original handler if defined - input.oninput?.(event); - }; + withValidation(element, options); } }); @@ -399,7 +351,6 @@ export class SimpleModal { exec(): void { if (!this.canClose) return; - if ( this.inputs .filter((i) => i.hidden !== true && i.optional !== true) @@ -409,7 +360,7 @@ export class SimpleModal { return; } - if (this.inputs.some((i) => i.indicator?.get() === "invalid")) { + if (this.inputs.some((i) => i.hasError === true)) { Notifications.add("Please solve all validation errors", 0); return; } diff --git a/frontend/static/languages/arabic_morocco.json b/frontend/static/languages/arabic_morocco.json new file mode 100644 index 000000000000..7304ee3ab8a9 --- /dev/null +++ b/frontend/static/languages/arabic_morocco.json @@ -0,0 +1,238 @@ +{ + "name": "arabic_morocco", + "rightToLeft": true, + "ligatures": true, + "orderedByFrequency": false, + "bcp47": "ar-MA", + "words": [ + "دابا", + "أنا", + "نتي", + "نتا", + "هو", + "هي", + "حنا", + "هما", + "واحد", + "جوج", + "ثلاثة", + "ربعة", + "خمسة", + "ستة", + "سبعة", + "تمنية", + "تسعود", + "عشرة", + "سلام", + "بسلامة", + "شكراً", + "لا شكراً", + "عفاك", + "واخا", + "لباس", + "بخير", + "بغيت", + "بغيتش", + "كانبغيك", + "عرفتش", + "سمح لي", + "فهمتش", + "فواكه", + "ريال", + "فلوس", + "دار", + "بيت", + "كوزينة", + "تواليت", + "فرماج", + "ساعة", + "صباح", + "الليل", + "اليوم", + "يمشي", + "يجي", + "ياكل", + "يشرب", + "يشوف", + "يدير", + "يهدر", + "يعرف", + "يخدم", + "ماشي", + "مشى", + "مشكل", + "زوين", + "خايب", + "عيان", + "فرحان", + "فين", + "شحال", + "آش", + "تفضل", + "تفضلو", + "صافي", + "بزاف", + "كبير", + "صغير", + "طويل", + "قصير", + "جديد", + "فقير", + "ضعيف", + "سخون", + "بارد", + "ساهل", + "صعيب", + "حلو", + "مر", + "نقي", + "موسخ", + "ثقيل", + "خفيف", + "ماضي", + "باين", + "بعيد", + "حسن", + "كثير", + "قليل", + "دائماً", + "أبداً", + "لبارح", + "غدا", + "العشية", + "الصباح", + "يمكن", + "لا", + "واه", + "مع", + "فوق", + "قدام", + "ورى", + "داخل", + "خارج", + "ليمـن", + "ليسـر", + "هنا", + "تما", + "علاش", + "شنو", + "مطيشة", + "هادي", + "سلو", + "كيفاش", + "زقرام", + "جلابة", + "القايد", + "الصهد", + "البرد", + "العجاج", + "صندالة", + "وقتاش", + "من", + "حتى", + "بين", + "عند", + "بلا", + "على", + "فـ", + "من بعد", + "قبل", + "تحت", + "قريب", + "شوية", + "سريع", + "محلول", + "مسدود", + "قديم", + "مزيان", + "صحيح", + "غلط", + "عدو", + "ولد", + "بنت", + "راجل", + "مرا", + "درّي", + "شاب", + "شيخ", + "زنقة", + "خدمة", + "طوموبيل", + "طنجية", + "سفنج", + "مقلي", + "مقشر", + "تران", + "طيارة", + "بحر", + "جبل", + "شمش", + "القمرة", + "نجمة", + "سما", + "الأرض", + "العافية", + "الريح", + "شتا", + "ثلج", + "صوت", + "كتاب", + "قلم", + "ورقة", + "باب", + "شرجم", + "كرسي", + "ملح", + "سكر", + "خبز", + "لحم", + "دجاج", + "حوت", + "ديسير", + "خضرة", + "تفاح", + "بانان", + "ليمون", + "ما", + "أتاي", + "قهوة", + "عصير", + "حليب", + "مطار", + "مدينة", + "دوّار", + "سوق", + "جامع", + "سبيطار", + "فارماسي", + "بوليس", + "بانكا", + "ريسطو", + "اوطيل", + "مدرسة", + "لافاك", + "صحاب", + "عائلة", + "با", + "خو", + "خت", + "مسمن", + "شرمولة", + "طوبة", + "بلاصة", + "طاكسي", + "فرطاسة", + "صرف", + "قرعة", + "غول", + "عجوز", + "شطة", + "مهراز", + "مقلة", + "درب", + "فراشة", + "مهرجان", + "شكارة", + "مقصودة" + ] +} diff --git a/frontend/static/languages/code_gleam.json b/frontend/static/languages/code_gleam.json new file mode 100644 index 000000000000..fb28fb003bf2 --- /dev/null +++ b/frontend/static/languages/code_gleam.json @@ -0,0 +1,448 @@ +{ + "name": "code_gleam", + "ligatures": true, + "words": [ + "!", + "!=", + "%", + "&&", + "*", + "+", + "-", + "->", + "..", + "/", + "<", + "<-", + "<<>>", + "<=", + "<>", + "==", + ">", + ">=", + "[]", + "#()", + "_", + "as", + "assert", + "auto", + "BitArray", + "Bool", + "case", + "const", + "delegate", + "derive", + "echo", + "else", + "external", + "False", + "Float", + "fn", + "if", + "implement", + "import", + "Int", + "let", + "List", + "macro", + "Nil", + "opaque", + "panic", + "pub", + "String", + "test", + "todo", + "True", + "type", + "use", + "||", + "|>", + "bit_array.append", + "bit_array.base16_decode", + "bit_array.base16_encode", + "bit_array.base64_decode", + "bit_array.base64_encode", + "bit_array.base64_url_decode", + "bit_array.base64_url_encode", + "bit_array.byte_size", + "bit_array.concat", + "bit_array.copy", + "bit_array.drop", + "bit_array.ends_with", + "bit_array.from_string", + "bit_array.inspect", + "bit_array.is_empty", + "bit_array.part", + "bit_array.slice", + "bit_array.starts_with", + "bit_array.to_string", + "bool.and", + "bool.exclusive_nor", + "bool.exclusive_or", + "bool.guard", + "bool.lazy_guard", + "bool.nand", + "bool.negate", + "bool.nor", + "bool.or", + "bytes_tree.append", + "bytes_tree.append_builder", + "bytes_tree.append_string", + "bytes_tree.byte_size", + "bytes_tree.concat", + "bytes_tree.from_bit_array", + "bytes_tree.from_string", + "bytes_tree.from_string_builder", + "bytes_tree.new", + "bytes_tree.prepend", + "bytes_tree.prepend_builder", + "bytes_tree.prepend_string", + "bytes_tree.to_bit_array", + "decode.at", + "decode.bit_array", + "decode.bool", + "decode.collapse_errors", + "decode.decode_error", + "decode.dict", + "decode.dynamic", + "decode.failure", + "decode.field", + "decode.float", + "decode.int", + "decode.list", + "decode.map", + "decode.map_errors", + "decode.new_primitive_decoder", + "decode.one_of", + "decode.optional", + "decode.optional_field", + "decode.optionally_at", + "decode.recursive", + "decode.run", + "decode.string", + "decode.subfield", + "decode.success", + "decode.Decoder", + "decode.DecodeError", + "dict.combine", + "dict.delete", + "dict.drop", + "dict.each", + "dict.filter", + "dict.fold", + "dict.from_list", + "dict.get", + "dict.has_key", + "dict.insert", + "dict.is_empty", + "dict.keys", + "dict.map_values", + "dict.merge", + "dict.new", + "dict.size", + "dict.take", + "dict.to_list", + "dict.upsert", + "dict.values", + "Dict", + "dynamic.array", + "dynamic.bit_array", + "dynamic.bool", + "dynamic.classify", + "dynamic.float", + "dynamic.from", + "dynamic.int", + "dynamic.list", + "dynamic.nil", + "dynamic.properties", + "dynamic.string", + "dynamic.Dynamic", + "float.absolute_value", + "float.add", + "float.ceiling", + "float.clamp", + "float.compare", + "float.divide", + "float.exponential", + "float.floor", + "float.logarithm", + "float.loosely_compare", + "float.loosely_equals", + "float.max", + "float.min", + "float.modulo", + "float.multiply", + "float.negate", + "float.parse", + "float.power", + "float.product", + "float.random", + "float.round", + "float.square_root", + "float.subtract", + "float.sum", + "float.to_precision", + "float.to_string", + "function.apply1", + "function.apply2", + "function.apply3", + "function.compose", + "function.constant", + "function.curry2", + "function.curry3", + "function.curry4", + "function.curry5", + "function.curry6", + "function.flip", + "function.identity", + "function.tap", + "Gt", + "int.absolute_value", + "int.add", + "int.base_parse", + "int.bitwise_and", + "int.bitwise_exclusive_or", + "int.bitwise_not", + "int.bitwise_or", + "int.bitwise_shift_left", + "int.bitwise_shift_right", + "int.clamp", + "int.compare", + "int.digits", + "int.divide", + "int.floor_divide", + "int.is_even", + "int.is_odd", + "int.max", + "int.min", + "int.modulo", + "int.multiply", + "int.negate", + "int.parse", + "int.power", + "int.product", + "int.random", + "int.remainder", + "int.square_root", + "int.subtract", + "int.to_base16", + "int.to_base2", + "int.to_base36", + "int.to_base8", + "int.to_base_string", + "int.to_float", + "int.to_string", + "int.undigits", + "io.debug", + "io.print", + "io.print_error", + "io.println", + "io.println_error", + "list.all", + "list.any", + "list.append", + "list.chunk", + "list.combination_pairs", + "list.combinations", + "list.contains", + "list.count", + "list.drop", + "list.drop_while", + "list.each", + "list.filter", + "list.filter_map", + "list.find", + "list.find_map", + "list.first", + "list.flat_map", + "list.flatten", + "list.fold", + "list.fold_right", + "list.fold_until", + "list.group", + "list.index_fold", + "list.index_map", + "list.interleave", + "list.intersperse", + "list.is_empty", + "list.key_filter", + "list.key_find", + "list.key_pop", + "list.key_set", + "list.last", + "list.length", + "list.map", + "list.map2", + "list.map_fold", + "list.max", + "list.min", + "list.partition", + "list.permutations", + "list.prepend", + "list.range", + "list.reduce", + "list.repeat", + "list.rest", + "list.reverse", + "list.sample", + "list.scan", + "list.shuffle", + "list.sized_chunk", + "list.sort", + "list.split", + "list.split_while", + "list.strict_zip", + "list.take", + "list.take_while", + "list.transpose", + "list.try_each", + "list.try_fold", + "list.try_map", + "list.unique", + "list.unzip", + "list.window", + "list.window_by_2", + "list.wrap", + "list.zip", + "Lt", + "None", + "Ok", + "option.all", + "option.flatten", + "option.from_result", + "option.is_none", + "option.is_some", + "option.lazy_or", + "option.lazy_unwrap", + "option.map", + "option.or", + "option.then", + "option.to_result", + "option.unwrap", + "option.values", + "Option", + "order.break_tie", + "order.compare", + "order.lazy_break_tie", + "order.negate", + "order.reverse", + "Order", + "pair.first", + "pair.map_first", + "pair.map_second", + "pair.new", + "pair.second", + "pair.swap", + "result.all", + "result.flatten", + "result.is_error", + "result.is_ok", + "result.lazy_or", + "result.lazy_unwrap", + "result.map", + "result.map_error", + "result.nil_error", + "result.or", + "result.partition", + "result.replace", + "result.replace_error", + "result.then", + "result.try", + "result.try_recover", + "result.unwrap", + "result.unwrap_both", + "result.unwrap_error", + "result.values", + "Result", + "set.contains", + "set.delete", + "set.difference", + "set.drop", + "set.each", + "set.filter", + "set.fold", + "set.from_list", + "set.insert", + "set.intersection", + "set.is_disjoint", + "set.is_empty", + "set.is_subset", + "set.map", + "set.new", + "set.size", + "set.symmetric_difference", + "set.take", + "set.to_list", + "set.union", + "Set", + "Some", + "string.append", + "string.byte_size", + "string.capitalise", + "string.compare", + "string.concat", + "string.contains", + "string.crop", + "string.drop_end", + "string.drop_start", + "string.ends_with", + "string.first", + "string.from_utf_codepoints", + "string.inspect", + "string.is_empty", + "string.join", + "string.last", + "string.length", + "string.lowercase", + "string.pad_end", + "string.pad_start", + "string.pop_grapheme", + "string.repeat", + "string.replace", + "string.reverse", + "string.slice", + "string.split", + "string.split_once", + "string.starts_with", + "string.to_graphemes", + "string.to_option", + "string.to_utf_codepoints", + "string.trim", + "string.trim_end", + "string.trim_start", + "string.uppercase", + "string.utf_codepoint", + "string.utf_codepoint_to_int", + "string_tree.append", + "string_tree.append_tree", + "string_tree.byte_size", + "string_tree.concat", + "string_tree.from_string", + "string_tree.from_strings", + "string_tree.is_empty", + "string_tree.is_equal", + "string_tree.join", + "string_tree.lowercase", + "string_tree.new", + "string_tree.prepend", + "string_tree.prepend_tree", + "string_tree.replace", + "string_tree.reverse", + "string_tree.split", + "string_tree.to_string", + "string_tree.uppercase", + "StringTree", + "uri.empty", + "uri.merge", + "uri.origin", + "uri.parse", + "uri.parse_query", + "uri.path_segments", + "uri.percent_decode", + "uri.percent_encode", + "uri.query_to_string", + "uri.to_string", + "Uri", + "UtfCodepoint" + ] +} diff --git a/frontend/static/quotes/code_java.json b/frontend/static/quotes/code_java.json index fb1375f88dc7..f6b4971f8817 100644 --- a/frontend/static/quotes/code_java.json +++ b/frontend/static/quotes/code_java.json @@ -32,7 +32,7 @@ "length": 62 }, { - "text": "public enum OperatingSystem {\n\tOSX, Windows, LINUX;\n\tpublic String toString() {\n\t\tswitch(this) {\n\t\t\tcase OSX: return \"Mac OS\",\n\t\t\tcase WINDOWS: return \"Windows\";\n\t\t\tcase LINUX: return \"Linux\";\n\t\t}\n\t}\n}", + "text": "public enum OperatingSystem {\n\tOSX, WINDOWS, LINUX;\n\tpublic String toString() {\n\t\tswitch(this) {\n\t\t\tcase OSX: return \"Mac OS\";\n\t\t\tcase WINDOWS: return \"Windows\";\n\t\t\tcase LINUX: return \"Linux\";\n\t\t}\n\t}\n}", "id": 5, "source": "Detect OS", "length": 201 @@ -50,10 +50,10 @@ "length": 545 }, { - "text": "public static int fibonacci(int n) {\n\tif (n <= 2) {\n\t\treturn 1;\n\t} else {\n\t\treturn fibonacci(n - 1) + fibonacci(n - 2);\n}", + "text": "public static int fibonacci(int n) {\n\tif (n <= 2) {\n\t\treturn 1;\n\t} else {\n\t\treturn fibonacci(n - 1) + fibonacci(n - 2);\n\t}\n}", "id": 8, "source": "Find the nth Fibonacci Number Recursively", - "length": 121 + "length": 124 }, { "text": "public static int binarySearch(int[] arr, int x) {\n\tint low = 0;\n\tint mid = 0;\n\tint high = arr.length - 1;\n\twhile (low <= high) {\n\t\tmid = (low + high) / 2;\n\t\tif (x == arr[mid]) {\n\t\t\treturn mid;\n\t\t} else if (x > arr[mid]) {\n\t\t\tlow = mid + 1;\n\t\t} else {\n\t\t\thigh = mid - 1;\n\t\t}\n\t}\n\treturn -1;\n}", diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index 14b7ec274ded..4074941f4cb7 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -38763,6 +38763,42 @@ "source": "The Pragmatic Programmer (2nd Edition)", "id": 7673, "length": 328 + }, + { + "text": "Every action you take is a vote for the type of person you wish to become. No single instance will transform your beliefs, but as the votes build up, so does the evidence of your new identity.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7674, + "length": 192 + }, + { + "text": "You do not rise to the level of your goals. You fall to the level of your systems.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7675, + "length": 82 + }, + { + "text": "You should be far more concerned with your current trajectory than with your current results.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7676, + "length": 93 + }, + { + "text": "When you fall in love with the process rather than the product, you don’t have to wait to give yourself permission to be happy. You can be satisfied anytime your system is running.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7677, + "length": 180 + }, + { + "text": "Goals are good for setting a direction, but systems are best for making progress.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7678, + "length": 81 + }, + { + "text": "All big things come from small beginnings. The seed of every habit is a single, tiny decision. But as that decision is repeated, a habit sprouts and grows stronger. Roots entrench themselves and branches grow. The task of breaking a bad habit is like uprooting a powerful oak within us. And the task of building a good habit is like cultivating a delicate flower one day at a time.", + "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", + "id": 7679, + "length": 381 } ] } diff --git a/frontend/static/quotes/polish.json b/frontend/static/quotes/polish.json index fadde2b0aa38..5e596a2470d3 100644 --- a/frontend/static/quotes/polish.json +++ b/frontend/static/quotes/polish.json @@ -1082,7 +1082,7 @@ "id": 182 }, { - "text": "Co chcesz przez to powiedzieć? Czy życzysz mi dobrego dnia, czy oznajmiasz, że dzień jest dobry, niezależnie od tego, co ja o nim myślę; czy sam dobrze się tego ranka czujesz, czy może uważasz, że dzisiaj należy być dobrym? – Wszystko naraz. A na dodatek, że w taki piękny dzień dobrze jest wypalić fajkę na świeżym powietrzu.", + "text": "Co chcesz przez to powiedzieć? Czy życzysz mi dobrego dnia, czy oznajmiasz, że dzień jest dobry, niezależnie od tego, co ja o nim myślę; czy sam dobrze się tego ranka czujesz, czy może uważasz, że dzisiaj należy być dobrym? - Wszystko naraz. A na dodatek, że w taki piękny dzień dobrze jest wypalić fajkę na świeżym powietrzu.", "source": "J.R.R. Tolkien, Hobbit, czyli tam i z powrotem", "length": 326, "id": 183 @@ -1368,6 +1368,96 @@ "source": "Polskie przysłowie", "length": 87, "id": 230 + }, + { + "text": "Ja nigdy nie będę służył żadnemu królowi ani cesarzowi, tylko mojej ojczyźnie.", + "source": "Tadeusz Kościuszko", + "length": 78, + "id": 231 + }, + { + "text": "Niczego w życiu nie należy się bać, należy to tylko zrozumieć.", + "source": "Maria Skłodowska-Curie", + "length": 62, + "id": 232 + }, + { + "text": "Dopóki nie skorzystałem z Internetu, nie wiedziałem, że na świecie jest tylu idiotów.", + "source": "Stanisław Lem", + "length": 85, + "id": 233 + }, + { + "text": "Matematyka to jedyna dziedzina, w której nie da się niczego ukryć.", + "source": "Hugo Steinhaus", + "length": 66, + "id": 234 + }, + { + "text": "Nie wystarczy mówić do rzeczy, trzeba jeszcze mówić do ludzi.", + "source": "Tadeusz Kotarbiński", + "length": 61, + "id": 235 + }, + { + "text": "Matematyka jest najpiękniejszym językiem świata.", + "source": "Stefan Banach", + "length": 48, + "id": 236 + }, + { + "text": "Nauka powinna służyć ludzkości, nie jej zagładzie.", + "source": "Józef Rotblat", + "length": 50, + "id": 237 + }, + { + "text": "Jeżeli na ciało nie działa żadna siła lub siły działające się równoważą, to ciało pozostaje w spoczynku lub porusza się ruchem jednostajnym prostoliniowym.", + "source": "I zasada dynamiki Newtona (Fizyka)", + "length": 155, + "id": 238 + }, + { + "text": "Jeżeli na ciało działa stała siła, to porusza się ono ruchem jednostajnie przyspieszonym, a przyspieszenie jest wprost proporcjonalne do siły działającej na to ciało, a odwrotnie proporcjonalne do masy ciała.", + "source": "II zasada dynamiki Newtona (Fizyka)", + "length": 208, + "id": 239 + }, + { + "text": "Każde dwa ciała przyciągają się wzajemnie siłą, której wartość jest proporcjonalna do iloczynu ich mas i odwrotnie proporcjonalna do kwadratu odległości między nimi.", + "source": "Prawo powszechnego ciążenia (Fizyka)", + "length": 165, + "id": 240 + }, + { + "text": "Jeśli ciało A działa na ciało B siłą, to ciało B działa na ciało A siłą o tej samej wartości, lecz przeciwnie skierowaną.", + "source": "III zasada dynamiki Newtona (Fizyka)", + "length": 121, + "id": 241 + }, + { + "text": "Natężenie prądu w przewodniku jest proporcjonalne do napięcia i odwrotnie proporcjonalne do oporu.", + "source": "Prawo Ohma (Fizyka)", + "length": 98, + "id": 242 + }, + { + "text": "Energia nie może być stworzona ani zniszczona - może jedynie zmieniać formę.", + "source": "Zasada zachowania energii (Fizyka)", + "length": 76, + "id": 243 + }, + { + "text": "Na ciało zanurzone w cieczy działa siła wyporu równa ciężarowi wypartej cieczy.", + "source": "Prawo Archimedesa (Fizyka)", + "length": 79, + "id": 244 + }, + { + "text": "Siła elektrostatyczna między dwoma ładunkami jest proporcjonalna do iloczynu wartości tych ładunków i odwrotnie proporcjonalna do kwadratu odległości między nimi.", + "source": "Prawo Coulomba (Fizyka)", + "length": 162, + "id": 245 } ] } diff --git a/frontend/static/themes/incognito.css b/frontend/static/themes/incognito.css index 0249d0726d1e..60a014ebcfa1 100644 --- a/frontend/static/themes/incognito.css +++ b/frontend/static/themes/incognito.css @@ -26,6 +26,14 @@ header #logo .icon svg path { fill: var(--text-color); } +/* fix logo clipping */ header #logo .icon { - padding-bottom: 0.1em; + padding-bottom: 0.05em; +} +nav { + padding-bottom: 0.2em; +} +header { + margin-bottom: -0.1em; } +/* */ diff --git a/frontend/static/themes/rainbow_trail.css b/frontend/static/themes/rainbow_trail.css index e18e62f5df45..9bc10e2a1a86 100644 --- a/frontend/static/themes/rainbow_trail.css +++ b/frontend/static/themes/rainbow_trail.css @@ -27,8 +27,22 @@ header #logo .text { -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: rainbow-gradient 30s alternate ease-in-out infinite; + padding-bottom: 0.1em; } +/* fix logo clipping */ +header #logo .icon { + padding-bottom: 0.05em; +} +nav { + padding-bottom: 0.2em; +} + +header { + margin-bottom: -0.1em; +} +/* */ + header #logo .top, .view-account .levelAndBar .level, [aria-label][data-balloon-pos]::after { diff --git a/frontend/static/themes/sewing_tin_light.css b/frontend/static/themes/sewing_tin_light.css index 314a26b57d72..b37243152ccf 100644 --- a/frontend/static/themes/sewing_tin_light.css +++ b/frontend/static/themes/sewing_tin_light.css @@ -34,8 +34,22 @@ header #logo .text { background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; + padding-bottom: 0.1em; } +/* fix logo clipping */ +header #logo .icon { + padding-bottom: 0.05em; +} +nav { + padding-bottom: 0.2em; +} + +header { + margin-bottom: -0.1em; +} +/* */ + header #logo .text .top { /* prevent it from being transparent */ -webkit-text-fill-color: #385eca; diff --git a/frontend/static/themes/suisei.css b/frontend/static/themes/suisei.css index 440b81aad65a..5923984d8ed8 100644 --- a/frontend/static/themes/suisei.css +++ b/frontend/static/themes/suisei.css @@ -30,6 +30,16 @@ header #logo .text { color: transparent; padding-bottom: 0.1em; } + +/* fix logo clipping */ +header #logo .icon { + padding-bottom: 0.05em; +} +header { + margin-bottom: -0.1em; +} +/* */ + header #logo .text .top { color: var(--main-color); } diff --git a/packages/schemas/src/languages.ts b/packages/schemas/src/languages.ts index 03ebde54a8dd..ed4d6f100b10 100644 --- a/packages/schemas/src/languages.ts +++ b/packages/schemas/src/languages.ts @@ -37,6 +37,7 @@ export const LanguageSchema = z.enum( "arabic_10k", "arabic_egypt", "arabic_egypt_1k", + "arabic_morocco", "malagasy", "malagasy_1k", "malay", @@ -397,6 +398,7 @@ export const LanguageSchema = z.enum( "code_arduino", "code_systemverilog", "code_elixir", + "code_gleam", "code_zig", "code_gdscript", "code_gdscript_2",