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: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @KinjiKawaguchi @araaki12345
* @KinjiKawaguchi @KikyoNanakusa
6 changes: 0 additions & 6 deletions typing-app/.eslintrc.json

This file was deleted.

6 changes: 6 additions & 0 deletions typing-app/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import eslintConfigPrettier from "eslint-config-prettier/flat";

const eslintConfig = [...nextCoreWebVitals, eslintConfigPrettier];

export default eslintConfig;
61 changes: 24 additions & 37 deletions typing-app/package.json
Original file line number Diff line number Diff line change
@@ -1,62 +1,49 @@
{
"name": "typing-app",
"version": "1.4.0",
"version": "1.4.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"format": "prettier . --write",
"format:ci": "prettier . --check",
"sanitize": "tsx scripts/sanitizeText.ts",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"next": "15.2.4",
"openapi-fetch": "0.13.3",
"prettier": "^3.4.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"next": "16.2.3",
"openapi-fetch": "0.17.0",
"react": "19.2.5",
"react-dom": "19.2.5",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
"sharp": "^0.34.5"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.3.1",
"@types/node": "^20.17.10",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "15.1.0",
"eslint-config-prettier": "^9.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"openapi-typescript": "6.7.6",
"postcss": "^8.4.49",
"sass": "^1.85.0",
"tailwindcss": "^3.4.16",
"tsx": "^4.19.3",
"typescript": "^5.7.2"
"@jest/globals": "^30.3.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.6.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "16.2.3",
"eslint-config-prettier": "^10.1.8",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
"openapi-typescript": "7.13.0",
"prettier": "^3.8.2",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: prettier moved from dependencies to devDependencies

In the old package.json, prettier was listed under dependencies. It's now correctly placed in devDependencies at line 38. This is a proper fix — prettier is a development tool and should not be bundled as a production dependency.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"sass": "^1.99.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
},
"repository": "https://github.com/su-its/typing",
"author": "su-its",
"packageManager": "yarn@4.6.0",
"engines": {
"npm": "use yarn instead"
},
"resolutions": {
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2"
}
}
6 changes: 0 additions & 6 deletions typing-app/postcss.config.js

This file was deleted.

10 changes: 6 additions & 4 deletions typing-app/src/app/game/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import fs from "fs";

const filenames = fs.readdirSync("public/texts/");

const getRandomSubjectText = () => {
const randomFilename = filenames[Math.floor(Math.random() * filenames.length)] ?? filenames[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: game/page.tsx: nullish coalescing fallback is ineffective when filenames is empty

At line 7, filenames[Math.floor(Math.random() * filenames.length)] ?? filenames[0] — the ?? filenames[0] fallback exists to satisfy noUncheckedIndexedAccess: true in tsconfig. However, if filenames is empty, Math.floor(Math.random() * 0) yields 0, filenames[0] is undefined, and the fallback filenames[0] is also undefined, so fs.readFileSync('public/texts/undefined') would throw. This is a pre-existing issue (the old code would also fail on an empty directory) and is unrealistic in practice since public/texts/ contains sample files.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

return fs.readFileSync(`public/texts/${randomFilename}`, "utf-8");
};

export default function Typing() {
const subjectText = fs.readFileSync(
`public/texts/${filenames[Math.floor(Math.random() * filenames.length)]}`,
"utf-8"
);
const subjectText = getRandomSubjectText();
const subjectTextOneLine = subjectText.replace(/\n/gm, " ");

return <GamePage subjectText={subjectTextOneLine} />;
Expand Down
54 changes: 33 additions & 21 deletions typing-app/src/components/organism/RankingTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import RankingTable from "../organism/RankingTable";
import { Pagination } from "../molecules/Pagination";
import RefreshButton from "../atoms/RefreshButton";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { client } from "@/libs/api";
import type { components } from "@/libs/api/v1";
import { showErrorToast } from "@/utils/toast";
Expand Down Expand Up @@ -53,30 +53,42 @@ const RankingTabs = () => {
const [rankingStartFrom, setRankingStartFrom] = useState(1);
const [sortBy, setSortBy] = useState<"accuracy" | "keystrokes">("accuracy");
const [totalRankingCount, setTotalRankingCount] = useState<number>(0);
const [refreshKey, setRefreshKey] = useState(0);

const LIMIT = 10; //TODO: Configファイルから取得

const fetchData = useCallback(async () => {
const { data, error } = await client.GET("/scores/ranking", {
params: {
query: {
sort_by: sortBy,
start: rankingStartFrom,
limit: LIMIT,
useEffect(() => {
let isCancelled = false;

const fetchData = async () => {
const { data, error } = await client.GET("/scores/ranking", {
params: {
query: {
sort_by: sortBy,
start: rankingStartFrom,
limit: LIMIT,
},
},
},
});
if (data) {
setScoreRankings(data.rankings);
setTotalRankingCount(data.total_count);
} else {
showErrorToast(error);
}
}, [sortBy, rankingStartFrom]);
});

useEffect(() => {
fetchData();
}, [fetchData]);
if (isCancelled) {
return;
}

if (data) {
setScoreRankings(data.rankings);
setTotalRankingCount(data.total_count);
} else {
showErrorToast(error);
}
};

void fetchData();

return () => {
isCancelled = true;
};
}, [refreshKey, sortBy, rankingStartFrom]);
Comment on lines +60 to +91
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: RankingTabs refactor fixes a pre-existing stale-data bug on refresh

The old code called fetchData() directly in the refresh button handler after setRankingStartFrom(1). Since fetchData was a useCallback capturing rankingStartFrom in its closure, the direct call would use the stale (pre-update) value of rankingStartFrom, not the newly-set value of 1. The new approach using refreshKey state to trigger a useEffect ensures the fetch always uses the committed state values. This is a correctness improvement, not just a refactor.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const handleTabChange = (index: number) => {
const sortOption = index === 0 ? "accuracy" : "keystrokes";
Expand Down Expand Up @@ -118,7 +130,7 @@ const RankingTabs = () => {
<RefreshButton
onClick={() => {
setRankingStartFrom(1);
fetchData();
setRefreshKey((prev) => prev + 1);
}}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion typing-app/src/components/templates/GameTyping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const GameTyping: React.FC<GameTypingProps> = ({ nextPage, subjectText, setScore
});

// 開始時刻と処理フラグの参照
const startTimeRef = useRef<number>(Date.now());
const startTimeRef = useRef<number>(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: startTimeRef initialization change relies on useEffect ordering guarantees

The change from useRef<number>(Date.now()) to useRef<number>(0) at typing-app/src/components/templates/GameTyping.tsx:34 moves the timer start initialization to a useEffect at line 193-196. This works correctly because: (1) the timer interval callback (line 110-121) won't fire until 100ms after mount, well after all effects complete; (2) keypress handlers only fire on user input, also after mount; (3) the completion check effect at line 127-132 checks stats.typeIndex === subjectText.length - 1, which is false on mount for any text longer than 1 character (all sample texts are 1000+ chars). The one theoretical edge case is if subjectText.length === 1, the completion effect would fire before the init effect (due to declaration order), reading startTimeRef.current = 0 and computing a wildly incorrect elapsed time (~55 years). This is unrealistic given the actual text files but worth noting as a fragile ordering dependency.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const isProcessingRef = useRef(false);

const typingQueueRef = useRef<number[]>([]);
Expand Down
Loading
Loading