AIdentity

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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:
Mapp/components/4_oil.tsx | 85+++++++++++++++++++++++++++++++++++++------------------------------------------
Mapp/components/6_chat.tsx | 302+++++++++++++++++++++++++++++++++++--------------------------------------------
Mapp/ctrl/page.tsx | 4++--
Mbun.lock | 12++++++------
Mpackage.json | 2+-
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" }