Skip to content
Open
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
9 changes: 9 additions & 0 deletions frontend/src/ts/components/layout/footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ export function Footer(): JSXElement {
}}
href="https://x.com/monkeytype"
/>
<Button
variant="text"
text="faq"
fa={{
icon: "fa-question-circle",
fixedWidth: true,
}}
onClick={() => showModal("Faq")}
/>
<Button
variant="text"
text="terms"
Expand Down
143 changes: 143 additions & 0 deletions frontend/src/ts/components/modals/FaqModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
createMemo,
createResource,
createSignal,
For,
JSXElement,
Show,
} from "solid-js";

import { cachedFetchJson } from "../../utils/json-data";
import { AnimatedModal } from "../common/AnimatedModal";

type FaqContentBlock =
| { type: "paragraph"; text: string }
| { type: "list"; items: string[] };

type FaqTopic = {
title: string;
searchText: string;
content: FaqContentBlock[];
};

type FaqData = {
topics: FaqTopic[];
};

async function getFaqData(): Promise<FaqData> {
return cachedFetchJson<FaqData>("/faq.json");
}

function RenderContent(props: { blocks: FaqContentBlock[] }): JSXElement {
return (
<div class="flex flex-col gap-4">
<For each={props.blocks}>
{(block) => (
<Show
when={block.type === "list"}
fallback={
<p>{(block as { type: "paragraph"; text: string }).text}</p>
}
>
<ul class="flex list-disc flex-col gap-2 pl-4">
<For each={(block as { type: "list"; items: string[] }).items}>
{(item) => <li>{item}</li>}
</For>
</ul>
</Show>
)}
</For>
</div>
);
}

export function FaqModal(): JSXElement {
const [selectedIndex, setSelectedIndex] = createSignal(0);
const [search, setSearch] = createSignal("");
const [faqData] = createResource(getFaqData);

const filteredTopics = createMemo(() => {
const topics = faqData()?.topics ?? [];
const q = search().toLowerCase().trim();
if (q === "") return topics.map((t, i) => ({ ...t, originalIndex: i }));
return topics
.map((t, i) => ({ ...t, originalIndex: i }))
.filter(
(t) =>
t.title.toLowerCase().includes(q) ||
t.searchText.toLowerCase().includes(q),
);
});

const effectiveIndex = createMemo(() => {
const topics = filteredTopics();
if (topics.length === 0) return -1;
const found = topics.find((t) => t.originalIndex === selectedIndex());
if (found !== undefined) return selectedIndex();
setSelectedIndex(topics[0]?.originalIndex ?? 0);
return topics[0]?.originalIndex ?? 0;
});

return (
<AnimatedModal
id="Faq"
title="FAQ"
modalClass="max-w-4xl h-[36rem] grid-rows-[auto_1fr]"
>
<div
class="-mt-1 grid h-full gap-3 overflow-hidden"
style={{ "grid-template-rows": "auto 1fr" }}
>
<input
type="text"
placeholder="Search..."
class="w-full rounded bg-sub-alt px-3 py-2 text-sm text-text placeholder-sub [color-scheme:dark] outline-none focus-visible:shadow-none"
value={search()}
onInput={(e) => {
setSearch(e.currentTarget.value);
}}
/>
<div class="grid min-h-0 flex-1 grid-cols-[12rem_1fr] gap-4 overflow-hidden">
<div class="flex min-h-0 flex-col gap-1 overflow-y-auto pr-2">
<Show
when={!faqData.loading}
fallback={<div class="p-2 text-sm text-sub">Loading...</div>}
>
<Show
when={filteredTopics().length > 0}
fallback={
<div class="p-2 text-sm text-sub">No results found.</div>
}
>
<For each={filteredTopics()}>
{(topic) => (
<button
type="button"
class="rounded p-2 text-left text-sm transition-colors"
classList={{
"bg-text text-bg":
effectiveIndex() === topic.originalIndex,
"text-sub hover:text-bg hover:bg-text":
effectiveIndex() !== topic.originalIndex,
}}
onClick={() => setSelectedIndex(topic.originalIndex)}
>
{topic.title}
</button>
)}
</For>
</Show>
</Show>
</div>
<div class="overflow-y-auto text-sm text-sub">
<Show when={effectiveIndex() !== -1 && !faqData.loading}>
<RenderContent
blocks={faqData()?.topics[effectiveIndex()]?.content ?? []}
/>
</Show>
</div>
</div>
</div>
</AnimatedModal>
);
}
2 changes: 2 additions & 0 deletions frontend/src/ts/components/modals/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { JSXElement, Show, Suspense, lazy } from "solid-js";

import { isDevEnvironment } from "../../utils/misc";
import { ContactModal } from "./ContactModal";
import { FaqModal } from "./FaqModal";
import { RegisterCaptchaModal } from "./RegisterCaptchaModal";
import { SupportModal } from "./SupportModal";
import { VersionHistoryModal } from "./VersionHistoryModal";
Expand All @@ -17,6 +18,7 @@ export function Modals(): JSXElement {
<ContactModal />
<RegisterCaptchaModal />
<SupportModal />
<FaqModal />
<Show when={isDevEnvironment()}>
<Suspense fallback={null}>
<DevOptionsModal />
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/ts/stores/modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type ModalId =
| "DevOptions"
| "DevInboxPicker"
| "RegisterCaptcha"
| "Alerts";
| "Alerts"
| "Faq";

export type ModalVisibility = {
visible: boolean;
Expand Down
182 changes: 182 additions & 0 deletions frontend/static/faq.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
{
"topics": [
{
"title": "How do I type coding symbols?",
"searchText": "coding symbols punctuation quotes custom text code special characters",
"content": [
{
"type": "paragraph",
"text": "There are two ways to type coding symbols in Monkeytype:"
},
{
"type": "list",
"items": [
"Punctuation mode - Enable it by clicking the @ punctuation button in the test config bar. This adds common symbols like ! , . ; : ' \" ( )",
"Quote mode - Switch to quote mode in the test config bar. Quotes often contain real code-like punctuation and symbols.",
"Custom text - Use the command line (Ctrl + Shift + P) and select \"custom text\" to type any text you want, including code."
]
}
]
},
{
"title": "How do I restart the test quickly?",
"searchText": "restart test quickly tab enter esc shortcut keyboard",
"content": [
{
"type": "paragraph",
"text": "You can restart the test without touching the mouse:"
},
{
"type": "list",
"items": [
"Press Tab + Enter - the default shortcut to restart.",
"Enable Quick Restart mode in settings - restart with Tab or Esc or Enter."
]
}
]
},
{
"title": "How do I change the language?",
"searchText": "language change switch foreign english spanish french german",
"content": [
{
"type": "paragraph",
"text": "Search language in command line. A list of all available languages will appear - select the one you want."
},
{
"type": "paragraph",
"text": "Monkeytype supports multiple languages."
}
]
},
{
"title": "How do I change the theme?",
"searchText": "theme color appearance dark light custom random palette",
"content": [
{
"type": "paragraph",
"text": "There are several ways to change the theme:"
},
{
"type": "list",
"items": [
"Open the command line with Ctrl + Shift + P and search for a theme name.",
"Go to Settings → Theme and browse the full list.",
"Enable random theme in settings to get a new theme on every test. After completing a test, the theme will be set to a random one. The random themes are not saved to your config. If set to 'favorite' only favorite themes will be randomized. If set to 'light' or 'dark', only presets with light or dark background colors will be randomized, respectively. If set to 'auto', dark or light themes are used depending on your system theme. If set to 'custom', custom themes will be randomized.",
"Click the palette icon in the footer to quickly switch themes."
]
}
]
},
{
"title": "How do I save my progress?",
"searchText": "save progress account history personal best login signup",
"content": [
{
"type": "paragraph",
"text": "To save your typing history and personal bests, create a free account."
},
{
"type": "paragraph",
"text": "Click the person icon in the top right to sign up or log in. Once logged in, all your results are automatically saved."
},
{
"type": "paragraph",
"text": "Without an account, results are only stored temporarily in your browser session."
}
]
},
{
"title": "How do the leaderboards work?",
"searchText": "leaderboard rank top fastest qualify anticheat english 15 60 seconds",
"content": [
{
"type": "paragraph",
"text": "The global leaderboards track the fastest typists for 15 second and 60 second English tests."
},
{
"type": "paragraph",
"text": "To qualify, your result must:"
},
{
"type": "list",
"items": [
"Be completed while logged in",
"Use the English language",
"Have no funbox modifiers active",
"Pass the anticheat verification"
]
}
]
},
{
"title": "What is Blind Mode?",
"searchText": "blind mode errors hide mistakes accuracy training",
"content": [
{
"type": "paragraph",
"text": "Blind mode hides your errors while typing - you won't see red characters or highlights during the test."
},
{
"type": "paragraph",
"text": "This is useful for training yourself to keep typing without fixating on mistakes. Accuracy is still tracked and shown at the end."
},
{
"type": "paragraph",
"text": "Enable it in Settings → Behavior → Blind Mode."
}
]
},
{
"title": "What is Pace Caret?",
"searchText": "pace caret target speed race wpm goal second caret",
"content": [
{
"type": "paragraph",
"text": "The pace caret is a second caret that moves at a target speed so you can race against it."
},
{
"type": "paragraph",
"text": "Set it to your personal best, average, or a custom WPM target in Settings → Caret → Pace Caret."
}
]
},
{
"title": "What are Funbox modes?",
"searchText": "funbox fun modes gibberish numbers challenge special modifier",
"content": [
{
"type": "paragraph",
"text": "Funbox modes are special modifiers that change how the test works. Examples:"
},
{
"type": "list",
"items": [
"gibberish - random nonsense words",
"58008 - only numbers",
"read ahead - hides the current word",
"no quit - forces you to finish"
]
},
{
"type": "paragraph",
"text": "Access via Ctrl + Shift + P → funbox."
}
]
},
{
"title": "How do I use Custom Text?",
"searchText": "custom text paste own code lyrics repeat randomize",
"content": [
{
"type": "paragraph",
"text": "Custom text lets you type any text you want - code, prose, lyrics."
},
{
"type": "paragraph",
"text": "Open the command line with Ctrl + Shift + P, search for custom text, and paste your content. You can also set it to repeat or randomize word order."
}
]
}
]
}