commit a2f3332b84e483006e3ccaf2d504cdd04d0106e0
parent 51335923b25bec767bc08e3a358d044a643b76ae
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Sat, 1 Nov 2025 10:51:56 +0900
refactor: Enhance stage transitions, component structure, and update tooling
Refactors component logic for clearer stage progression in `OilArtCanvasWrapper` and `QuestionAnswer`. `QuestionAnswer` now ensures automatic advancement after audio playback and removes unused `forwardRef`. Updates `eslint` dependency and streamlines code formatting across the codebase.
Diffstat:
5 files changed, 183 insertions(+), 222 deletions(-)
diff --git a/app/components/4_oil.tsx b/app/components/4_oil.tsx
@@ -1,24 +1,24 @@
-import { Canvas } from '@react-three/fiber';
-import { OrbitControls } from '@react-three/drei';
-import { OilArtPlane, OilArtAPI } from './OilArt/OilArtPlane.tsx';
-import { useEffect, useRef, useState } from 'react';
-import * as THREE from 'three';
-import { StageProps } from '../ctrl/page.tsx';
-
+import { Canvas } from "@react-three/fiber";
+import { OrbitControls } from "@react-three/drei";
+import { OilArtAPI, OilArtPlane } from "./OilArt/OilArtPlane.tsx";
+import { useEffect, useRef, useState } from "react";
+import * as THREE from "three";
+import { StageProps } from "../ctrl/page.tsx";
// --- オーディオ関連の定数とカスタムフック ---
const AUDIO_SOURCES: Record<number, string> = {
- 1: '/audio/001.wav',
- 2: '/audio/002.wav',
+ 1: "/audio/001.wav",
+ 2: "/audio/002.wav",
};
-
// シーケンシャルな音声再生と状態遷移を制御するカスタムフック (修正版)
-const useSequentialAudio = (initialState: number, audioSources: Record<number, string>, onComplete: () => void) => {
+const useSequentialAudio = (
+ initialState: number,
+ audioSources: Record<number, string>,
+ onComplete: () => void,
+) => {
const [sceneState, setSceneState] = useState(initialState);
const audioRef = useRef<HTMLAudioElement | null>(null);
-
// 現在の状態に対応する音源のURLを取得
const currentAudioUrl = audioSources[sceneState];
-
// 音声再生のロジック
useEffect(() => {
// --- 1. 終了条件のチェック ---
@@ -28,14 +28,13 @@ const useSequentialAudio = (initialState: number, audioSources: Record<number, s
onComplete();
return;
}
-
// 現在の状態に対応する音源がない場合、ここで処理を停止
if (!currentAudioUrl) {
return;
}
-
- console.log(`Starting setup for state ${sceneState}: ${currentAudioUrl}`);
-
+ console.log(
+ `Starting setup for state ${sceneState}: ${currentAudioUrl}`,
+ );
// --- 2. 古いAudioオブジェクトのクリーンアップ ---
const existingAudio = audioRef.current;
if (existingAudio) {
@@ -45,76 +44,72 @@ const useSequentialAudio = (initialState: number, audioSources: Record<number, s
// `oncanplaythrough`リスナーはここでは不要ですが、もしあれば削除すべきです。
// Audioオブジェクトのライフサイクルを明確にするため、毎回新しいインスタンスを生成します。
}
-
// --- 3. 新しいAudioオブジェクトの作成と設定 ---
const audio = new Audio(currentAudioUrl);
audioRef.current = audio;
-
// 再生が終了したときのハンドラを定義
const handleAudioEnded = () => {
console.log(`Audio ${sceneState} finished. Transitioning state.`);
// 次の状態へ遷移 (例: 1 -> 2, 2 -> 3)
- setSceneState(prev => prev + 1);
+ setSceneState((prev) => prev + 1);
};
-
- audio.addEventListener('ended', handleAudioEnded);
-
+ audio.addEventListener("ended", handleAudioEnded);
// --- 4. 再生の開始 ---
// ロードやイベントを待たずに、すぐに再生を試みる
- audio.play().catch(e => {
- console.warn(`Audio playback failed for state ${sceneState} (requires user interaction):`, e);
+ audio.play().catch((e) => {
+ console.warn(
+ `Audio playback failed for state ${sceneState} (requires user interaction):`,
+ e,
+ );
// ユーザーに最初のクリックを促すメッセージなどを表示すると良い
});
-
-
// --- 5. クリーンアップ関数 ---
return () => {
// アンマウント/状態遷移時に現在のAudioオブジェクトを確実に停止し、リスナーを削除
if (audio === audioRef.current) { // 現在設定したAudioオブジェクトであることを確認
audio.pause();
- audio.removeEventListener('ended', handleAudioEnded);
+ audio.removeEventListener("ended", handleAudioEnded);
}
};
-
}, [sceneState, audioSources, onComplete]); // sceneStateとcurrentAudioUrlは基本的に連動するため、sceneStateを依存配列に含める
-
return sceneState;
};
-
-export default function OilArtCanvasWrapper({onComplete}:StageProps) {
+export default function OilArtCanvasWrapper({ onComplete }: StageProps) {
const apiRef = useRef<OilArtAPI>({} as OilArtAPI);
-
const sceneState = useSequentialAudio(1, AUDIO_SOURCES, onComplete);
-
useEffect(() => {
// 外部API経由でランダムな滴下を定期的に実行
const intervalId = setInterval(() => {
if (apiRef.current.dropOil) {
const x = Math.random() * 2 - 1; // -1.0〜1.0
const y = Math.random() * 2 - 1; // -1.0〜1.0
- const color = new THREE.Color(Math.random(), Math.random(), Math.random());
+ const color = new THREE.Color(
+ Math.random(),
+ Math.random(),
+ Math.random(),
+ );
apiRef.current.dropOil({ x, y }, color);
-
// 傾きもランダムに変更
- if (sceneState == 2){
+ if (sceneState == 2) {
const tiltX = Math.random() * 0.8 - 0.4;
const tiltY = Math.random() * 0.8 - 0.4;
apiRef.current.tilt({ x: tiltX, y: tiltY });
- }else{
- apiRef.current.tilt({ x: 0, y: 0});
+ } else {
+ apiRef.current.tilt({ x: 0, y: 0 });
}
}
}, 1500);
-
return () => clearInterval(intervalId);
}, []);
-
return (
<Canvas camera={{ position: [0, 0, 1] }}>
- <color attach="background" args={[0x000000]} />
- {/* OilArtPlaneに外部制御用のAPIを渡す */}
- <OilArtPlane onTriggerAPI={(api) => (apiRef.current = api)} />
- <OrbitControls enableZoom={false} enablePan={false} enableRotate={false} />
+ <color attach="background" args={[0x000000]} />
+ <OilArtPlane onTriggerAPI={(api) => (apiRef.current = api)} />
+ <OrbitControls
+ enableZoom={false}
+ enablePan={false}
+ enableRotate={false}
+ />
</Canvas>
);
}
diff --git a/app/components/6_chat.tsx b/app/components/6_chat.tsx
@@ -1,4 +1,4 @@
-import React, {
+import {
forwardRef,
KeyboardEvent,
useEffect,
@@ -6,6 +6,7 @@ import React, {
useRef,
useState,
} from "react";
+import { StageProps } from "../ctrl/page.tsx";
// 質問データ型を拡張し、音源のURLを追加
export type QuestionWithAudio = {
input: string; // 質問文
@@ -13,173 +14,140 @@ export type QuestionWithAudio = {
audioUrl: string; // 質問に関連付けられた音源のURL
};
// 親コンポーネントからアクセスするためのRef型
-export interface QuestionAnswerRef {
- // 外部からの操作は不要なため空のまま
-}
interface QuestionAnswerProps {
questions: QuestionWithAudio[];
onComplete: () => void; // 全質問完了時のコールバック
}
-const QuestionAnswer = forwardRef<QuestionAnswerRef, QuestionAnswerProps>(
- ({ questions, onComplete }, ref) => {
- // 現在の質問のインデックス
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
- // ユーザーの現在の回答
- const [currentAnswer, setCurrentAnswer] = useState("");
- // エラーメッセージ
- const [errorMessage, setErrorMessage] = useState("");
- // Audioオブジェクトのインスタンスを保持するためのRef
- const audioRef = useRef<HTMLAudioElement | null>(null);
-
- // **注**: 最新のステートを参照するためのRefは、回答完了時の複雑なロジックを削除するため、ここでは不要になりますが、
- // 回答ロジックのシンプルな遷移のために、念のため残しておきます。
- const latestStateRef = useRef({
- answerLength: 0,
- questionIndex: 0,
- isAnswerComplete: false,
- });
-
- const currentQuestion = questions[currentQuestionIndex];
-
- if (!currentQuestion) {
- return <div className="p-4">🎉 すべての質問が完了しました!</div>;
+const QuestionAnswer: React.FC<QuestionAnswerProps> = (
+ { questions, onComplete },
+) => {
+ // 現在の質問のインデックス
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
+ // ユーザーの現在の回答
+ const [currentAnswer, setCurrentAnswer] = useState("");
+ // エラーメッセージ
+ const [errorMessage, setErrorMessage] = useState("");
+ // Audioオブジェクトのインスタンスを保持するためのRef
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+ // **注**: 最新のステートを参照するためのRefは、回答完了時の複雑なロジックを削除するため、ここでは不要になりますが、
+ // 回答ロジックのシンプルな遷移のために、念のため残しておきます。
+ const latestStateRef = useRef({
+ answerLength: 0,
+ questionIndex: 0,
+ isAnswerComplete: false,
+ });
+ const currentQuestion = questions[currentQuestionIndex];
+ const { input: questionText, ans: correctAnswer, audioUrl } =
+ currentQuestion;
+ const nextCorrectKey = correctAnswer[currentAnswer.length];
+ // ステートが更新されるたびにRefを更新する (回答完了判定には使わない)
+ useEffect(() => {
+ latestStateRef.current = {
+ answerLength: currentAnswer.length,
+ questionIndex: currentQuestionIndex,
+ isAnswerComplete: currentAnswer.length === correctAnswer.length,
+ };
+ }, [currentAnswer.length, currentQuestionIndex]);
+ useEffect(() => {
+ // 既存のAudioがあれば、onendedリスナーを解除して停止
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.currentTime = 0;
+ audioRef.current.onended = null;
}
-
- const { input: questionText, ans: correctAnswer, audioUrl } =
- currentQuestion;
- const nextCorrectKey = correctAnswer[currentAnswer.length];
-
- // ステートが更新されるたびにRefを更新する (回答完了判定には使わない)
- useEffect(() => {
- latestStateRef.current = {
- answerLength: currentAnswer.length,
- questionIndex: currentQuestionIndex,
- isAnswerComplete: currentAnswer.length === correctAnswer.length,
- };
- }, [currentAnswer.length, currentQuestionIndex, correctAnswer.length]);
-
- // Refから親コンポーネントに公開するメソッド(今回はnextQuestionの外部からの使用は非推奨)
- useImperativeHandle(ref, () => ({
- // ここでは何もしない
- }));
-
- // 質問インデックスが変更されたとき、またはコンポーネントがマウントされたときに音源をロード/再生
- useEffect(() => {
- // 既存のAudioがあれば、onendedリスナーを解除して停止
+ // 新しいAudioオブジェクトを作成または既存のものを使用
+ const audio = audioRef.current || new Audio();
+ audioRef.current = audio;
+ // 新しい音源をロード
+ audio.src = audioUrl;
+ audio.load();
+ // 再生終了時のハンドラ: 無条件に次の質問へ強制移行するロジックに変更
+ audio.onended = (event: Event) => {
+ // 再生終了時の最新の質問インデックスを取得
+ const currentQuestionIdx = latestStateRef.current.questionIndex;
+ const nextIndex = currentQuestionIdx + 1;
+ if (nextIndex < questions.length) {
+ // 次の質問へ (音源再生終了を待って強制的に移行)
+ setCurrentQuestionIndex(nextIndex);
+ setCurrentAnswer("");
+ setErrorMessage("");
+ } else {
+ // 最後の質問の音源再生終了後、onCompleteを実行して終了
+ onComplete();
+ }
+ };
+ // 新しい音源の再生を開始
+ audio.play().catch((e) =>
+ console.error("Audio playback failed on initial play:", e)
+ );
+ // クリーンアップ関数
+ return () => {
if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.currentTime = 0;
audioRef.current.onended = null;
- }
-
- // 新しいAudioオブジェクトを作成または既存のものを使用
- const audio = audioRef.current || new Audio();
- audioRef.current = audio;
-
- // 新しい音源をロード
- audio.src = audioUrl;
- audio.load();
-
- // 再生終了時のハンドラ: 無条件に次の質問へ強制移行するロジックに変更
- audio.onended = (event: Event) => {
- // 再生終了時の最新の質問インデックスを取得
- const currentQuestionIdx = latestStateRef.current.questionIndex;
- const nextIndex = currentQuestionIdx + 1;
-
- if (nextIndex < questions.length) {
- // 次の質問へ (音源再生終了を待って強制的に移行)
- setCurrentQuestionIndex(nextIndex);
- setCurrentAnswer("");
- setErrorMessage("");
- } else {
- // 最後の質問の音源再生終了後、onCompleteを実行して終了
- onComplete();
- }
- };
-
- // 新しい音源の再生を開始
- audio.play().catch((e) =>
- console.error("Audio playback failed on initial play:", e)
- );
-
- // クリーンアップ関数
- return () => {
- if (audioRef.current) {
- audioRef.current.onended = null;
- audioRef.current.pause();
- }
- };
- }, [currentQuestionIndex, questions.length, audioUrl]); // 依存配列は音源切り替えに必要なもののみ
-
- // キー入力時のハンドラ: 回答ロジックのみを保持し、音源制御は削除
- const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
- // エンターキーなど、特殊なキーのデフォルト動作を防ぐ
- if (e.key === "Enter") {
- e.preventDefault();
- return;
- }
-
- // 回答が既に完了している場合は入力を無視
- if (currentAnswer.length >= correctAnswer.length) {
- e.preventDefault();
- return;
- }
-
- if (e.key === nextCorrectKey) {
- e.preventDefault(); // デフォルトの入力をキャンセル
- setErrorMessage(""); // エラーをクリア
-
- const newAnswer = currentAnswer + e.key;
- setCurrentAnswer(newAnswer);
-
- // 回答完了後も、音源が終了するまで質問は切り替わらない
- if (newAnswer.length === correctAnswer.length) {
- // 回答完了時の視覚的な遅延のみを保持 (音源制御は行わない)
- // *注意*: 音源終了時に自動で次の質問へ進むため、ここでは何もしません
- }
- } else {
- e.preventDefault(); // デフォルトの入力をキャンセル
- setErrorMessage("🚫 その回答は正しくありません");
+ audioRef.current.pause();
}
};
-
- return (
- <div>
- <p>
- {questionText}
- </p>
-
- <div>
- <input
- type="text"
- value={currentAnswer}
- onKeyDown={handleKeyDown}
- readOnly // カスタムロジックで値をセットするため、readonlyにする
- placeholder={correctAnswer.split("").map(() => "_")
- .join(" ")}
- autoFocus // 自動フォーカス
- />
- {/* 回答が正しい場合に緑色の枠線を表示 */}
- {currentAnswer.length === correctAnswer.length && (
- <span></span>
- )}
- </div>
-
- {errorMessage && (
- <p>
- {errorMessage}
- </p>
- )}
-
+ }, [currentQuestionIndex, questions.length, audioUrl]); // 依存配列は音源切り替えに必要なもののみ
+ if (!currentQuestion) {
+ return <div className="p-4">🎉 すべての質問が完了しました!</div>;
+ }
+ // キー入力時のハンドラ: 回答ロジックのみを保持し、音源制御は削除
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
+ // エンターキーなど、特殊なキーのデフォルト動作を防ぐ
+ if (e.key === "Enter") {
+ e.preventDefault();
+ return;
+ }
+ // 回答が既に完了している場合は入力を無視
+ if (currentAnswer.length >= correctAnswer.length) {
+ e.preventDefault();
+ return;
+ }
+ if (e.key === nextCorrectKey) {
+ e.preventDefault(); // デフォルトの入力をキャンセル
+ setErrorMessage(""); // エラーをクリア
+ const newAnswer = currentAnswer + e.key;
+ setCurrentAnswer(newAnswer);
+ // 回答完了後も、音源が終了するまで質問は切り替わらない
+ if (newAnswer.length === correctAnswer.length) {
+ // 回答完了時の視覚的な遅延のみを保持 (音源制御は行わない)
+ // *注意*: 音源終了時に自動で次の質問へ進むため、ここでは何もしません
+ }
+ } else {
+ e.preventDefault(); // デフォルトの入力をキャンセル
+ setErrorMessage("🚫 その回答は正しくありません");
+ }
+ };
+ return (
+ <article style={{}}>
+ <p style={{ textAlign: "center", fontSize: "5rem" }}>
+ {questionText}
+ </p>
+ <div style={{ textAlign: "center" }}>
+ <input
+ type="text"
+ value={currentAnswer}
+ onKeyDown={handleKeyDown}
+ readOnly // カスタムロジックで値をセットするため、readonlyにする
+ placeholder={correctAnswer.split("").map(() => "_")
+ .join(" ")}
+ autoFocus // 自動フォーカス
+ style={{ fontSize: "2rem" }}
+ />
+ {/* 回答が正しい場合に緑色の枠線を表示 */}
+ {currentAnswer.length === correctAnswer.length && <span></span>}
+ </div>
+ {errorMessage && (
<p>
- (日本語の回答は**ヘボン式ローマ字**のキー入力のみを想定しています)
+ {errorMessage}
</p>
- </div>
- );
- },
-);
-QuestionAnswer.displayName = "QuestionAnswer";
-// ダミーの音源URLを持つ拡張された質問データ
+ )}
+ <p style={{ textAlign: "center", padding: "5vh" }}>
+ (日本語の回答は**ヘボン式ローマ字**のキー入力のみを想定しています)
+ </p>
+ </article>
+ );
+};
const myQuestions: QuestionWithAudio[] = [
{ input: "5 + 9 = ?", ans: "14", audioUrl: "/audio/001.wav" }, // 実際には適切なURLに置き換えてください
{
@@ -189,23 +157,21 @@ const myQuestions: QuestionWithAudio[] = [
},
{ input: "Next.jsの親要素は?", ans: "react", audioUrl: "/audio/003.wav" },
];
-// StagePropsの定義がないため、ここでは仮の定義を使用します
-interface StageProps {
- onComplete: () => void;
-}
export default function QuizPage({ onComplete }: StageProps) {
- const questionAnswerRef = useRef<QuestionAnswerRef>(null);
-
const handleQuizComplete = () => {
- alert("全ての質問に正しく回答しました!お疲れ様でした!");
- // QuizPageに渡されたonCompleteを実行
onComplete();
};
-
return (
- <div style={{ display: "flex" }}>
+ <div
+ style={{
+ width: "100vw",
+ height: "100vh",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ }}
+ >
<QuestionAnswer
- ref={questionAnswerRef}
questions={myQuestions}
onComplete={handleQuizComplete}
/>
diff --git a/app/ctrl/page.tsx b/app/ctrl/page.tsx
@@ -31,7 +31,7 @@ const SixthChat = dynamic<StageProps>(()=>import('../components/6_chat.tsx'),{
const SeventhVR = dynamic<StageProps>(()=>import('../components/7_vr.tsx'),{
loading: Loading,
});
-const EighthLightActivity = dynamic<StageProps>(()=>import('../components/8_lightactivity.tsx'),{
+const EighthLightActivity = dynamic<StageProps>(()=>import('../components/8_flameText.tsx'),{
loading: Loading,
});
const Error = dynamic<StageProps>(() => import('../components/error.tsx'), {
@@ -43,7 +43,7 @@ export default function Play() {
const pageRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isInitialCheckDone, setIsInitialCheckDone] = useState(false);
- const [ stage, setStage ] = useState(1);
+ const [ stage, setStage ] = useState(2);
const handleStageComplete = useCallback(() => {
setStage(prevStage => prevStage + 1);
}, []);
diff --git a/bun.lock b/bun.lock
@@ -18,7 +18,7 @@
"@types/node": "^20.19.24",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.0",
"eslint-config-next": "15.5.5",
"typescript": "^5.9.3",
},
@@ -83,17 +83,17 @@
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
- "@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="],
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
- "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
- "@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="],
+ "@eslint/js": ["@eslint/js@9.39.0", "", {}, "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
- "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -683,7 +683,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="],
+ "eslint": ["eslint@9.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.0", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg=="],
"eslint-config-next": ["eslint-config-next@15.5.5", "", { "dependencies": { "@next/eslint-plugin-next": "15.5.5", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-f8lRSSelp6cqrYjxEMjJ5En3WV913gTu/w9goYShnIujwDSQlKt4x9MwSDiduE9R5mmFETK44+qlQDxeSA0rUA=="],
diff --git a/package.json b/package.json
@@ -24,7 +24,7 @@
"@types/node": "^20.19.24",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.0",
"eslint-config-next": "15.5.5",
"typescript": "^5.9.3"
}