Skip to content

Commit 6ce3160

Browse files
committed
feat: frontend perf improvement
1 parent 9916c3a commit 6ce3160

File tree

5 files changed

+98
-46
lines changed

5 files changed

+98
-46
lines changed

frontend/components/game/article.tsx

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,9 @@ import { useContextSelector } from "use-context-selector";
99

1010
import CustomGameBanner from "@caviardeul/components/game/customGameBanner";
1111
import { GameContext } from "@caviardeul/components/game/gameManager";
12+
import WordContainer from "@caviardeul/components/game/word";
1213
import { SettingsContext } from "@caviardeul/components/settings/manager";
13-
import {
14-
isCommonWord,
15-
isSelected,
16-
isWord,
17-
splitWords,
18-
standardizeText,
19-
} from "@caviardeul/utils/caviarding";
20-
21-
const WordContainer_: React.FC<{ word: string }> = ({ word }) => {
22-
const [isOver, revealedWords, selection] = useContextSelector(
23-
GameContext,
24-
(context) => [context.isOver, context.revealedWords, context.selection],
25-
);
26-
if (word === undefined) {
27-
return null;
28-
}
29-
30-
const standardizedWord = standardizeText(word);
31-
const revealed = isOver || revealedWords.has(standardizedWord);
32-
const selected = selection && isSelected(standardizedWord, selection[0]);
33-
34-
if (revealed) {
35-
return (
36-
<span className={`word` + (selected ? " selected" : "")}>{word}</span>
37-
);
38-
} else {
39-
return (
40-
<span className="word caviarded" data-word-length={word.length}>
41-
{"█".repeat(word.length)}
42-
</span>
43-
);
44-
}
45-
};
46-
47-
const WordContainer = React.memo(WordContainer_);
14+
import { isCommonWord, isWord, splitWords } from "@caviardeul/utils/caviarding";
4815

4916
const parseHTML = (content: string): ReactNode => {
5017
const doc = parse(content);
@@ -121,17 +88,12 @@ const parseText = (text: string): ReactNode => {
12188
});
12289
};
12390

124-
const ArticleContainer = () => {
125-
const [article, selection] = useContextSelector(GameContext, (context) => [
126-
context.article,
127-
context.selection,
128-
]);
91+
const AutoScrollerManager = () => {
12992
const { settings } = useContext(SettingsContext);
13093
const { autoScroll } = settings;
131-
132-
const inner = useMemo(
133-
() => (article ? parseHTML(article.content) : null),
134-
[article],
94+
const selection = useContextSelector(
95+
GameContext,
96+
(context) => context.selection,
13597
);
13698

13799
// Scroll to selection
@@ -141,7 +103,7 @@ const ArticleContainer = () => {
141103
const articleContainer =
142104
document.querySelector<HTMLElement>(".article-container");
143105
const elements =
144-
articleContainer?.querySelectorAll<HTMLElement>(".word.selected");
106+
articleContainer?.querySelectorAll<HTMLElement>(".word .selected");
145107
if (elements?.length) {
146108
const element = elements[index % elements.length];
147109
const y = element.offsetTop;
@@ -153,6 +115,16 @@ const ArticleContainer = () => {
153115
}
154116
}, [selection, autoScroll]);
155117

118+
return null;
119+
};
120+
121+
const ArticleContainer = () => {
122+
const article = useContextSelector(GameContext, (context) => context.article);
123+
const inner = useMemo(
124+
() => (article ? parseHTML(article.content) : null),
125+
[article],
126+
);
127+
156128
if (!article) {
157129
return null;
158130
}
@@ -162,6 +134,7 @@ const ArticleContainer = () => {
162134
return (
163135
<div className="article-container">
164136
{custom && <CustomGameBanner safetyLevel={safety} />}
137+
<AutoScrollerManager />
165138
{inner}
166139
</div>
167140
);

frontend/components/game/word.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useMemo } from "react";
2+
import { useContextSelector } from "use-context-selector";
3+
4+
import { GameContext } from "@caviardeul/components/game/gameManager";
5+
import { isSelected, standardizeText } from "@caviardeul/utils/caviarding";
6+
7+
const useStandardizedWord = (word: string) => {
8+
return useMemo(() => standardizeText(word), [word]);
9+
};
10+
11+
const useIsSelected = (word: string) => {
12+
const standardizedWord = useStandardizedWord(word);
13+
const selection = useContextSelector(
14+
GameContext,
15+
(context) => context.selection?.[0],
16+
);
17+
return !!(selection && isSelected(standardizedWord, selection));
18+
};
19+
20+
const useIsRevealed = (word: string) => {
21+
const standardizedWord = useStandardizedWord(word);
22+
return useContextSelector(GameContext, (context) =>
23+
context.revealedWords.has(standardizedWord),
24+
);
25+
};
26+
27+
const SelectedWrapper: React.FC<{
28+
word: string;
29+
children: React.ReactNode;
30+
}> = React.memo(({ word, children }) => {
31+
const selected = useIsSelected(word);
32+
if (!selected) {
33+
return children;
34+
}
35+
return <span className="selected">{children}</span>;
36+
});
37+
38+
const RevealedWord = React.memo<{ word: string }>(({ word }) => {
39+
return (
40+
<span className="word">
41+
<SelectedWrapper word={word}>{word}</SelectedWrapper>
42+
</span>
43+
);
44+
});
45+
46+
const CaviardedWord = React.memo<{ word: string }>(({ word }) => {
47+
return (
48+
<span className="word caviarded" data-word-length={word.length}>
49+
{"█".repeat(word.length)}
50+
</span>
51+
);
52+
});
53+
54+
const MaybeRevealedWord = React.memo<{ word: string }>(({ word }) => {
55+
const revealed = useIsRevealed(word);
56+
if (revealed) {
57+
return <RevealedWord word={word} />;
58+
}
59+
return <CaviardedWord word={word} />;
60+
});
61+
62+
const WordContainer = React.memo<{ word: string }>(({ word }) => {
63+
const isOver = useContextSelector(GameContext, (context) => context.isOver);
64+
if (word === undefined) {
65+
return null;
66+
}
67+
68+
if (isOver) {
69+
return <RevealedWord word={word} />;
70+
}
71+
return <MaybeRevealedWord word={word} />;
72+
});
73+
74+
export default WordContainer;

frontend/eslint.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const config = [
3434

3535
rules: {
3636
"prettier/prettier": "error",
37+
"react/display-name": "off",
38+
"react/prop-types": "off",
3739
"no-unused-vars": [
3840
"error",
3941
{

frontend/styles/style.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ main {
465465
border: 1px solid transparent;
466466
white-space: nowrap;
467467

468-
&.selected {
468+
.selected {
469469
color: $lightBlue;
470470
}
471471
}

frontend/utils/caviarding.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ export const buildAlternatives = (standardizedWord: string): string[] =>
127127
* @param selection
128128
*/
129129
export const isSelected = (word: string, selection: string): boolean => {
130+
if (!word.startsWith(selection)) {
131+
return false;
132+
}
130133
if (word === selection) {
131134
return true;
132135
} else {

0 commit comments

Comments
 (0)