Skip to content

Commit 686d7a3

Browse files
committed
feat(footer): add FAQ modal (@openvaibhav)
1 parent ea1f586 commit 686d7a3

2 files changed

Lines changed: 261 additions & 250 deletions

File tree

frontend/src/ts/components/modals/FaqModal.tsx

Lines changed: 79 additions & 250 deletions
Original file line numberDiff line numberDiff line change
@@ -1,244 +1,66 @@
1-
import { createMemo, createSignal, For, JSXElement, Show } from "solid-js";
1+
import {
2+
createMemo,
3+
createResource,
4+
createSignal,
5+
For,
6+
JSXElement,
7+
Show,
8+
} from "solid-js";
29

10+
import { cachedFetchJson } from "../../utils/json-data";
311
import { AnimatedModal } from "../common/AnimatedModal";
412

13+
type FaqContentBlock =
14+
| { type: "paragraph"; text: string }
15+
| { type: "list"; items: string[] };
16+
517
type FaqTopic = {
618
title: string;
7-
content: JSXElement;
819
searchText: string;
20+
content: FaqContentBlock[];
921
};
1022

11-
const faqTopics: FaqTopic[] = [
12-
{
13-
title: "How do I type coding symbols?",
14-
searchText:
15-
"coding symbols punctuation quotes custom text code special characters",
16-
content: (
17-
<div class="flex flex-col gap-4">
18-
<p>There are two ways to type coding symbols in Monkeytype:</p>
19-
<ul class="flex list-disc flex-col gap-2 pl-4">
20-
<li>
21-
<span class="text-text">Punctuation mode</span> - Enable it by
22-
clicking the <span class="text-text">@ punctuation</span> button in
23-
the test config bar. This adds common symbols like{" "}
24-
<span class="text-text">! , . ; : &apos; &quot; ( )</span>
25-
</li>
26-
<li>
27-
<span class="text-text">Quote mode</span> - Switch to{" "}
28-
<span class="text-text">quote</span> mode in the test config bar.
29-
Quotes often contain real code-like punctuation and symbols.
30-
</li>
31-
<li>
32-
<span class="text-text">Custom text</span> - Use the command line{" "}
33-
<span class="text-text">(Ctrl + Shift + P)</span> and select
34-
&quot;custom text&quot; to type any text you want, including code.
35-
</li>
36-
</ul>
37-
</div>
38-
),
39-
},
40-
{
41-
title: "How do I restart the test quickly?",
42-
searchText: "restart test quickly tab enter esc shortcut keyboard",
43-
content: (
44-
<div class="flex flex-col gap-4">
45-
<p>You can restart the test without touching the mouse:</p>
46-
<ul class="flex list-disc flex-col gap-2 pl-4">
47-
<li>
48-
Press <span class="text-text">Tab + Enter</span> - the default
49-
shortcut to restart.
50-
</li>
51-
<li>
52-
Enable <span class="text-text">Quick Restart</span> mode in settings
53-
- restart with <span class="text-text">Tab or Esc or Enter</span>.
54-
</li>
55-
</ul>
56-
</div>
57-
),
58-
},
59-
{
60-
title: "How do I change the language?",
61-
searchText: "language change switch foreign english spanish french german",
62-
content: (
63-
<div class="flex flex-col gap-4">
64-
<p>
65-
Search <span class="text-text">language</span> in command line. A list
66-
of all available languages will appear - select the one you want.
67-
</p>
68-
<p>Monkeytype supports multiple languages.</p>
69-
</div>
70-
),
71-
},
72-
{
73-
title: "How do I change the theme?",
74-
searchText: "theme color appearance dark light custom random palette",
75-
content: (
76-
<div class="flex flex-col gap-4">
77-
<p>There are several ways to change the theme:</p>
78-
<ul class="flex list-disc flex-col gap-2 pl-4">
79-
<li>
80-
Open the <span class="text-text">command line</span> with{" "}
81-
<span class="text-text">Ctrl + Shift + P</span> and search for a
82-
theme name.
83-
</li>
84-
<li>
85-
Go to <span class="text-text">Settings → Theme</span> and browse the
86-
full list.
87-
</li>
88-
<li>
89-
Enable <span class="text-text">random theme</span> in settings to
90-
get a new theme on every test. After completing a test, the theme
91-
will be set to a random one. The random themes are not saved to your
92-
config. If set to &apos;favorite&apos; only favorite themes will be
93-
randomized. If set to &apos;light&apos; or &apos;dark&apos;, only
94-
presets with light or dark background colors will be randomized. If
95-
set to &apos;auto&apos;, dark or light themes are used depending on
96-
your system theme. If set to &apos;custom&apos;, custom themes will
97-
be randomized.
98-
</li>
99-
<li>
100-
Click the <span class="text-text">palette icon</span> in the footer
101-
to quickly switch themes.
102-
</li>
103-
</ul>
104-
</div>
105-
),
106-
},
107-
{
108-
title: "How do I save my progress?",
109-
searchText: "save progress account history personal best login signup",
110-
content: (
111-
<div class="flex flex-col gap-4">
112-
<p>
113-
To save your typing history and personal bests, create a free account.
114-
</p>
115-
<p>
116-
Click the <span class="text-text">person icon</span> in the top right
117-
to sign up or log in. Once logged in, all your results are
118-
automatically saved.
119-
</p>
120-
<p>
121-
Without an account, results are only stored temporarily in your
122-
browser session.
123-
</p>
124-
</div>
125-
),
126-
},
127-
{
128-
title: "How do the leaderboards work?",
129-
searchText:
130-
"leaderboard rank top fastest qualify anticheat english 15 60 seconds",
131-
content: (
132-
<div class="flex flex-col gap-4">
133-
<p>
134-
The global leaderboards track the fastest typists for{" "}
135-
<span class="text-text">15 second</span> and{" "}
136-
<span class="text-text">60 second</span> English tests.
137-
</p>
138-
<p>To qualify, your result must:</p>
139-
<ul class="flex list-disc flex-col gap-2 pl-4">
140-
<li>Be completed while logged in</li>
141-
<li>
142-
Use the <span class="text-text">English</span> language
143-
</li>
144-
<li>Have no funbox modifiers active</li>
145-
<li>Pass the anticheat verification</li>
146-
</ul>
147-
</div>
148-
),
149-
},
150-
{
151-
title: "What is Blind Mode?",
152-
searchText: "blind mode errors hide mistakes accuracy training",
153-
content: (
154-
<div class="flex flex-col gap-4">
155-
<p>
156-
Blind mode hides your errors while typing - you won&apos;t see red
157-
characters or highlights during the test.
158-
</p>
159-
<p>
160-
This is useful for training yourself to keep typing without fixating
161-
on mistakes. Accuracy is still tracked and shown at the end.
162-
</p>
163-
<p>
164-
Enable it in{" "}
165-
<span class="text-text">Settings → Behavior → Blind Mode</span>.
166-
</p>
167-
</div>
168-
),
169-
},
170-
{
171-
title: "What is Pace Caret?",
172-
searchText: "pace caret target speed race wpm goal second caret",
173-
content: (
174-
<div class="flex flex-col gap-4">
175-
<p>
176-
The pace caret is a second caret that moves at a target speed so you
177-
can race against it.
178-
</p>
179-
<p>
180-
Set it to your personal best, average, or a custom WPM target in{" "}
181-
<span class="text-text">Settings → Caret → Pace Caret</span>.
182-
</p>
183-
</div>
184-
),
185-
},
186-
{
187-
title: "What are Funbox modes?",
188-
searchText: "funbox fun modes gibberish numbers challenge special modifier",
189-
content: (
190-
<div class="flex flex-col gap-4">
191-
<p>
192-
Funbox modes are special modifiers that change how the test works.
193-
Examples:
194-
</p>
195-
<ul class="flex list-disc flex-col gap-2 pl-4">
196-
<li>
197-
<span class="text-text">gibberish</span> - random nonsense words
198-
</li>
199-
<li>
200-
<span class="text-text">58008</span> - only numbers
201-
</li>
202-
<li>
203-
<span class="text-text">read ahead</span> - hides the current word
204-
</li>
205-
<li>
206-
<span class="text-text">no quit</span> - forces you to finish
207-
</li>
208-
</ul>
209-
<p>
210-
Access via <span class="text-text">Ctrl + Shift + P</span> → funbox.
211-
</p>
212-
</div>
213-
),
214-
},
215-
{
216-
title: "How do I use Custom Text?",
217-
searchText: "custom text paste own code lyrics repeat randomize",
218-
content: (
219-
<div class="flex flex-col gap-4">
220-
<p>
221-
Custom text lets you type any text you want - code, prose, lyrics.
222-
</p>
223-
<p>
224-
Open the command line with{" "}
225-
<span class="text-text">Ctrl + Shift + P</span>, search for{" "}
226-
<span class="text-text">custom text</span>, and paste your content.
227-
You can also set it to repeat or randomize word order.
228-
</p>
229-
</div>
230-
),
231-
},
232-
];
23+
type FaqData = {
24+
topics: FaqTopic[];
25+
};
26+
27+
async function getFaqData(): Promise<FaqData> {
28+
return cachedFetchJson<FaqData>("/faq.json");
29+
}
30+
31+
function RenderContent(props: { blocks: FaqContentBlock[] }): JSXElement {
32+
return (
33+
<div class="flex flex-col gap-4">
34+
<For each={props.blocks}>
35+
{(block) => (
36+
<Show
37+
when={block.type === "list"}
38+
fallback={
39+
<p>{(block as { type: "paragraph"; text: string }).text}</p>
40+
}
41+
>
42+
<ul class="flex list-disc flex-col gap-2 pl-4">
43+
<For each={(block as { type: "list"; items: string[] }).items}>
44+
{(item) => <li>{item}</li>}
45+
</For>
46+
</ul>
47+
</Show>
48+
)}
49+
</For>
50+
</div>
51+
);
52+
}
23353

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

23859
const filteredTopics = createMemo(() => {
60+
const topics = faqData()?.topics ?? [];
23961
const q = search().toLowerCase().trim();
240-
if (q === "") return faqTopics.map((t, i) => ({ ...t, originalIndex: i }));
241-
return faqTopics
62+
if (q === "") return topics.map((t, i) => ({ ...t, originalIndex: i }));
63+
return topics
24264
.map((t, i) => ({ ...t, originalIndex: i }))
24365
.filter(
24466
(t) =>
@@ -269,7 +91,7 @@ export function FaqModal(): JSXElement {
26991
<input
27092
type="text"
27193
placeholder="Search..."
272-
class="w-full rounded bg-sub-alt px-3 py-2 text-sm text-text placeholder-sub outline-none focus-visible:shadow-none"
94+
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"
27395
value={search()}
27496
onInput={(e) => {
27597
setSearch(e.currentTarget.value);
@@ -278,33 +100,40 @@ export function FaqModal(): JSXElement {
278100
<div class="grid min-h-0 flex-1 grid-cols-[12rem_1fr] gap-4 overflow-hidden">
279101
<div class="flex min-h-0 flex-col gap-1 overflow-y-auto pr-2">
280102
<Show
281-
when={filteredTopics().length > 0}
282-
fallback={
283-
<div class="p-2 text-sm text-sub">No results found.</div>
284-
}
103+
when={!faqData.loading}
104+
fallback={<div class="p-2 text-sm text-sub">Loading...</div>}
285105
>
286-
<For each={filteredTopics()}>
287-
{(topic) => (
288-
<button
289-
type="button"
290-
class="rounded p-2 text-left text-sm transition-colors"
291-
classList={{
292-
"bg-text text-bg":
293-
effectiveIndex() === topic.originalIndex,
294-
"text-sub hover:text-bg hover:bg-text":
295-
effectiveIndex() !== topic.originalIndex,
296-
}}
297-
onClick={() => setSelectedIndex(topic.originalIndex)}
298-
>
299-
{topic.title}
300-
</button>
301-
)}
302-
</For>
106+
<Show
107+
when={filteredTopics().length > 0}
108+
fallback={
109+
<div class="p-2 text-sm text-sub">No results found.</div>
110+
}
111+
>
112+
<For each={filteredTopics()}>
113+
{(topic) => (
114+
<button
115+
type="button"
116+
class="rounded p-2 text-left text-sm transition-colors"
117+
classList={{
118+
"bg-text text-bg":
119+
effectiveIndex() === topic.originalIndex,
120+
"text-sub hover:text-bg hover:bg-text":
121+
effectiveIndex() !== topic.originalIndex,
122+
}}
123+
onClick={() => setSelectedIndex(topic.originalIndex)}
124+
>
125+
{topic.title}
126+
</button>
127+
)}
128+
</For>
129+
</Show>
303130
</Show>
304131
</div>
305132
<div class="overflow-y-auto text-sm text-sub">
306-
<Show when={effectiveIndex() !== -1}>
307-
{faqTopics[effectiveIndex()]?.content}
133+
<Show when={effectiveIndex() !== -1 && !faqData.loading}>
134+
<RenderContent
135+
blocks={faqData()?.topics[effectiveIndex()]?.content ?? []}
136+
/>
308137
</Show>
309138
</div>
310139
</div>

0 commit comments

Comments
 (0)