Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-failure-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: pr_num
path: ./pr_num

- name: Read the pr_num file
id: pr_num_reader
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/write-labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: labels
path: ./labels/

- name: Read json file
id: json_reader
Expand Down
4 changes: 2 additions & 2 deletions backend/__tests__/api/controllers/user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3347,7 +3347,7 @@ describe("user controller test", () => {
bio: new Array(251).fill("x").join(""),
keyboard: new Array(76).fill("x").join(""),
socialProfiles: {
twitter: new Array(21).fill("x").join(""),
twitter: new Array(16).fill("x").join(""),
github: new Array(40).fill("x").join(""),
website: `https://${new Array(201 - "https://".length)
.fill("x")
Expand All @@ -3362,7 +3362,7 @@ describe("user controller test", () => {
validationErrors: [
'"bio" String must contain at most 250 character(s)',
'"keyboard" String must contain at most 75 character(s)',
'"socialProfiles.twitter" String must contain at most 20 character(s)',
'"socialProfiles.twitter" String must contain at most 15 character(s)',
'"socialProfiles.github" String must contain at most 39 character(s)',
'"socialProfiles.website" String must contain at most 200 character(s)',
],
Expand Down
10 changes: 7 additions & 3 deletions docs/LANGUAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ The contents of the file should be as follows:
{
"name": string,
"rightToLeft": boolean,
"ligatures": boolean,
"joiningScript": boolean,
"orderedByFrequency": boolean,
"bcp47": string,
"words": string[]
}
```

It is recommended that you familiarize yourselves with JSON before adding a language. For the `name` field, put the name of your language. `rightToLeft` indicates how the language is written. If it is written right to left then put `true`, otherwise put `false`.
`ligatures` A ligature occurs when multiple letters are joined together to form a character [more details](<https://en.wikipedia.org/wiki/Ligature_(writing)>). If there's joining in the words, which is the case in languages like (Arabic, Malayalam, Persian, Sanskrit, Central_Kurdish... etc.), then set the value to `true`, otherwise set it to `false`. For `bcp47` put your languages [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag). If the words you're adding are ordered by frequency (most common words at the top, least at the bottom) set the value of `orderedByFrequency` to `true`, otherwise `false`. Finally, add your list of words to the `words` field.
It is recommended that you familiarize yourselves with JSON before adding a language. For the `name` field, put the name of your language.
`rightToLeft` indicates how the language is written. If it is written right to left then put `true`, otherwise put `false`.
`joiningScript` indicates whether the language requires joining letters to render correctly. Set it to `true` if characters must join with surrounding characters or if their shapes change based on position in a word (initial, medial, final, or isolated), or if they use connecting marks (matras/vowel signs) that reshape the base characters. Otherwise, set it to `false.`
For `bcp47` put your languages [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag).
If the words you're adding are ordered by frequency (most common words at the top, least at the bottom) set the value of `orderedByFrequency` to `true`, otherwise `false`.
Finally, add your list of words to the `words` field.

Then, go to `packages/schemas/src/languages.ts` and add your new language name at the _end_ of the `LanguageSchema` enum. Make sure to end the line with a comma. Make sure to add all your language names if you have created multiple word lists of differing lengths in the same language.

Expand Down
25 changes: 25 additions & 0 deletions docs/SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Table of contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Quickstart](#quickstart)
- [Hosting over the network (HTTPS)](#hosting-over-the-network-https)
- [Account System](#account-system)
- [Setup Firebase](#setup-firebase)
- [Update backend configuration](#update-backend-configuration)
Expand All @@ -36,6 +37,30 @@
- run `docker compose up -d`
- after the command exits successfully you can access [http://localhost:8080](http://localhost:8080)

### Hosting over the network (HTTPS)

If you plan to access your self-hosted Monkeytype instance over a local network or the internet (not using `localhost`), **you must serve it over HTTPS**. Modern browsers restrict key web features, such as `crypto.randomUUID`, to secure contexts. Accessing the site via HTTP over a network will cause the frontend to crash with errors like `Uncaught TypeError: crypto.randomUUID is not a function`.

To solve this, you need to place a reverse proxy (like Nginx, Caddy, or Traefik) in front of your containers to handle HTTPS/TLS termination.

#### Troubleshooting Frontend Connection Issues

If your reverse proxy is up but you see errors like `Looks like the server is experiencing unexpected down time` or network errors when fetching resources, your frontend is likely trying to communicate with the backend over unsecure HTTP, causing a **Mixed Content** block in the browser.

Ensure you configure the frontend to talk to your secure backend URL by following these rules in your `.env` file:

1. **Update the frontend and backend URL:** Set `MONKEYTYPE_FRONTENDURL` and `MONKEYTYPE_BACKENDURL` to your full HTTPS backend domain.
2. **Do not include a trailing slash:** Ensure the URL does not end with a `/` (e.g., use `https://api.yourdomain.com`, **not** `https://api.yourdomain.com/`). A trailing slash will cause `404 Not Found` errors due to double slashes in the API calls (like `//configuration`).
3. **Force container recreation:** Monkeytype is a Single Page Application (SPA), meaning environment variables are baked into the static JavaScript files during startup. If you change your `.env`, you must completely recreate the container for the changes to apply:

```bash
docker compose up -d --force-recreate
```

> [!TIP]
> After updating your configuration and recreating the containers, clear your browser cache or perform a hard reload (Ctrl + F5) to make sure your browser isn't running an old cached version of the frontend.


## Account System

By default, user sign-up and login are disabled. To enable this, you'll need to set up a Firebase project.
Expand Down
18 changes: 8 additions & 10 deletions frontend/__tests__/components/ui/form/SubmitButton.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ import { render, screen } from "@solidjs/testing-library";
import { JSXElement } from "solid-js";
import { describe, it, expect } from "vitest";

import { SubmitButton } from "../../../../src/ts/components/ui/form/SubmitButton";
import {
FormStateSlice,
SubmitButton,
} from "../../../../src/ts/components/ui/form/SubmitButton";

type FormState = {
canSubmit: boolean;
isSubmitting: boolean;
isValid: boolean;
isDirty: boolean;
};
type FormState = FormStateSlice;

function makeForm(state: Partial<FormState> = {}) {
const fullState: FormState = {
canSubmit: true,
isSubmitting: false,
isValid: true,
isDirty: true,
isDefaultValue: false,
...state,
};

Expand All @@ -39,9 +37,9 @@ describe("SubmitButton", () => {
expect(screen.getByRole("button")).toHaveAttribute("type", "submit");
});

it("disables when form is not dirty", () => {
it("disables when form is unchanged", () => {
render(() => (
<SubmitButton form={makeForm({ isDirty: false })} text="Save" />
<SubmitButton form={makeForm({ isDefaultValue: true })} text="Save" />
));
expect(screen.getByRole("button")).toBeDisabled();
});
Expand Down
75 changes: 0 additions & 75 deletions frontend/src/html/popups.html
Original file line number Diff line number Diff line change
Expand Up @@ -266,78 +266,3 @@
<div class="suggestions"></div>
</div>
</dialog>

<dialog id="editProfileModal" class="modalWrapper hidden">
<form class="modal">
<div class="title">Edit Profile</div>
<div>
<label>name</label>
<div>
To update your name, go to Account Settings > Account > Update account
name
</div>
</div>
<div>
<label>avatar</label>
<div>
To update your avatar make sure your Discord account is linked, then go
to Account Settings > Account > Discord Integration and click "Update
Avatar"
</div>
</div>
<div>
<label>bio</label>
<textarea class="bio" autocomplete="off" maxlength="250"></textarea>
</div>
<div>
<label>keyboard</label>
<textarea class="keyboard" autocomplete="off" maxlength="75"></textarea>
</div>
<div>
<label>github</label>
<div class="socialURL">
<p>https://github.com/</p>
<input
class="github"
type="text"
value=""
placeholder="username"
maxlength="39"
/>
</div>
</div>
<div>
<label>twitter</label>
<div class="socialURL">
<p>https://x.com/</p>
<input
class="twitter"
type="text"
value=""
placeholder="username"
maxlength="20"
/>
</div>
</div>
<div>
<label>website</label>
<input class="website" type="text" value="" maxlength="200" />
</div>
<div>
<label>badge</label>
<div class="badgeSelectionContainer"></div>
</div>
<div>
<label>public activity</label>
<label class="checkbox">
<input
class="editProfileShowActivityOnPublicProfile"
type="checkbox"
checked
/>
<span>Include test activity graph on your public profile.</span>
</label>
</div>
<button class="edit-profile-submit" type="submit">save</button>
</form>
</dialog>
59 changes: 0 additions & 59 deletions frontend/src/styles/popups.scss
Original file line number Diff line number Diff line change
Expand Up @@ -582,62 +582,3 @@ body.darkMode {
}
}
}

#editProfileModal {
.modal {
max-width: 600px;
max-height: 100%;
label {
color: var(--sub-color);
margin-bottom: 0.25em;
display: block;
}
input:not([type="checkbox"]) {
width: 100%;
}
input[type="checkbox"] {
vertical-align: text-bottom;
}
textarea {
resize: vertical;
width: 100%;
padding: 10px;
line-height: 1.2rem;
min-height: 5rem;
max-height: 10rem;
}

.socialURL {
display: flex;
}

.socialURL > p {
margin-block: 0.5rem;
margin-inline-end: 0.5rem;
}

.badgeSelectionContainer {
display: flex;
flex-wrap: wrap;
}

.badgeSelectionItem {
width: max-content;
opacity: 25%;
cursor: pointer;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
padding: 0;
border-radius: calc(var(--roundness) / 2);
}

.badgeSelectionItem.selected,
.badgeSelectionItem:hover {
opacity: 100%;
}

span {
color: var(--text-color);
}
}
}
24 changes: 12 additions & 12 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@
unicode-bidi: bidi-override;
}
}
&.withLigatures {
&.joiningScript {
.word {
overflow-wrap: anywhere;
padding-bottom: 0.05em; // compensate for letter border
Expand Down Expand Up @@ -531,8 +531,8 @@
&.typed-effect-dots {
/* transform already typed letters into appropriately colored dots */

&:not(.withLigatures) .word,
&.withLigatures .word.broken-ligatures {
&:not(.joiningScript) .word,
&.joiningScript .word.broken-joining {
letter {
position: relative;
display: inline-block;
Expand All @@ -550,17 +550,17 @@
}
}
// unify dot spacing
&.withLigatures .word.broken-ligatures {
&.joiningScript .word.broken-joining {
letter {
width: 0.4em;
}
}
.word.broken-ligatures:not(.needs-wrap) {
.word.broken-joining:not(.needs-wrap) {
white-space: nowrap;
}

&:not(.withLigatures) .word.typed,
&.withLigatures .word.broken-ligatures.typed {
&:not(.joiningScript) .word.typed,
&.joiningScript .word.broken-joining.typed {
letter {
color: var(--bg-color);
animation: typedEffectToDust 200ms ease-out 0ms 1 forwards !important;
Expand All @@ -571,18 +571,18 @@
}
}

&:not(.withLigatures):not(.blind) {
&:not(.joiningScript):not(.blind) {
.word letter.incorrect::after {
background: var(--c-dot--error);
}
}
&.withLigatures:not(.blind) .word.broken-ligatures letter.incorrect::after {
&.joiningScript:not(.blind) .word.broken-joining letter.incorrect::after {
background: var(--c-dot--error);
}

@media (prefers-reduced-motion) {
&:not(.withLigatures) .word.typed,
&.withLigatures .word.broken-ligatures.typed {
&:not(.joiningScript) .word.typed,
&.joiningScript .word.broken-joining.typed {
letter {
animation: none !important;
transform: scale(0.4);
Expand Down Expand Up @@ -874,7 +874,7 @@
unicode-bidi: bidi-override;
}
}
&.withLigatures {
&.joiningScript {
.word {
overflow-wrap: anywhere;
padding-bottom: 2px; // compensate for letter border
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/ts/components/common/UserBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function UserBadge(props: {
class?: string;
balloon?: Omit<BalloonProps, "text">;
hideTextOnSmallScreens?: boolean;
hideDescription?: boolean;
}): JSXElement {
const badge = (): UserBadgeType | undefined =>
props.id !== undefined ? badges[props.id] : undefined;
Expand All @@ -24,7 +25,7 @@ export function UserBadge(props: {
"rounded-[0.5em] px-[0.5em] py-[0.25em] text-em-xs",
props.class,
)}
text={badge()?.description ?? ""}
text={props.hideDescription ? "" : (badge()?.description ?? "")}
{...props.balloon}
style={{
background: badge()?.background ?? "inherit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function CustomTestDurationModal(): JSXElement {
form={form}
variant="button"
text="apply"
skipDirtyCheck
skipUnchangedCheck
/>
</form>
</AnimatedModal>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/ts/components/modals/CustomTextModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ export function CustomTextModal(): JSXElement {
</div>
<SubmitButton
form={form}
skipDirtyCheck
skipUnchangedCheck
variant="button"
text="ok"
class="lg:col-start-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function CustomWordAmountModal(): JSXElement {
form={form}
variant="button"
text="apply"
skipDirtyCheck
skipUnchangedCheck
/>
</form>
</AnimatedModal>
Expand Down
Loading
Loading