syurein commited on
Commit
a09ca12
·
1 Parent(s): 016f062
static/script.js CHANGED
@@ -3,65 +3,109 @@
3
  "use strict"; // より厳格なエラーチェック
4
 
5
  // --- グローバル変数 ---
6
- let learningData = null; // 学習データ (learning.html用)
7
  let currentItemIndex = 0; // 現在表示中のアイテムインデックス (learning.html用)
8
  let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary' (learning.html用)
9
  let correctEffectTimeout; // 正解エフェクト非表示用のタイマーID (learning.html用)
10
  let correctEffect = null; // 正解エフェクト要素 (learning.html用)
 
 
11
 
12
  // --- 共通関数 ---
13
 
14
  /**
15
- * 指定されたURLに遷移します。
16
  * @param {string} url - 遷移先のURL
17
  */
18
  function navigateTo(url) {
19
- window.location.href = url;
 
 
 
 
20
  }
21
 
22
  /**
23
- * メニューボタンがクリックされたときの処理(仮)
24
- * TODO: 実際のメニューUIを実装する
25
  */
26
  function openMenu() {
27
- console.log("Menu button clicked. Implement menu display logic here.");
28
- alert("メニュー機能は未実装です。\nフッターのナビゲーションを使用してください。");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
 
31
  /**
32
  * ローディングスピナーを表示/非表示します。
33
  * @param {boolean} show - trueで表示、falseで非表示
34
  * @param {string} buttonId - 操作対象のボタンID (input.html用)
35
  */
36
  function toggleLoading(show, buttonId = 'generate-button') {
37
- const generateButton = document.getElementById(buttonId);
38
- if (!generateButton) return;
39
 
40
- const spinner = generateButton.querySelector('.loading-spinner');
41
- const buttonText = generateButton.querySelector('.button-text');
 
 
42
 
43
- if (show) {
44
- generateButton.disabled = true;
45
- if (spinner) spinner.style.display = 'inline-block';
46
- if (buttonText) buttonText.textContent = '生成中...';
47
- } else {
48
- generateButton.disabled = false;
49
- if (spinner) spinner.style.display = 'none';
50
- if (buttonText) buttonText.textContent = '生成する'; // 元のテキストに戻す
 
51
  }
52
 
53
- // learning.html 用のローディング表示/非表示
54
  const loadingIndicator = document.getElementById('mode-indicator');
55
  const cardElement = document.getElementById('learning-card');
56
  const paginationElement = document.querySelector('.pagination');
57
  const optionsArea = document.getElementById('options-area');
58
-
59
- if (pathname === '/learning') { // learning.htmlの場合のみ
60
- if (loadingIndicator) loadingIndicator.textContent = show ? '読み込み中...' : ''; // モード表示はdisplayCurrentItemで行う
61
- if (loadingIndicator) loadingIndicator.classList.toggle('loading', show);
62
- if (cardElement) cardElement.style.opacity = show ? '0.5' : '1';
63
- if (paginationElement) paginationElement.style.display = show ? 'none' : 'flex';
64
- if (optionsArea) optionsArea.style.display = show ? 'none' : 'block'; // 初期はblockだがdisplayCurrentItemで調整
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
  }
67
 
@@ -74,13 +118,19 @@ function displayErrorMessage(message, elementId = 'error-message') {
74
  const errorElement = document.getElementById(elementId);
75
  if (errorElement) {
76
  errorElement.textContent = message;
 
77
  errorElement.style.display = message ? 'block' : 'none';
 
 
 
 
 
78
  }
79
  }
80
 
81
  // --- 画面遷移用関数 ---
82
  function goToInput() {
83
- navigateTo('/input'); // input.html はルートパスに割り当てる想定
84
  }
85
 
86
  function goToHistory() {
@@ -93,10 +143,12 @@ function goToSettings() {
93
 
94
  function goToLearning(contentId) {
95
  if (contentId) {
 
96
  navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
97
  } else {
98
  console.error('goToLearning requires a content ID.');
99
- alert('学習コンテンツIDが見つかりません。');
 
100
  }
101
  }
102
 
@@ -109,96 +161,118 @@ function goToLearning(contentId) {
109
  async function handleGenerateSubmit() {
110
  const urlInput = document.getElementById('youtube-url');
111
  const youtubeUrl = urlInput.value.trim();
112
- displayErrorMessage(''); // 前のエラーメッセージをクリア
 
113
 
114
  if (!youtubeUrl) {
115
- displayErrorMessage('YouTubeリンクを入力してください。');
116
- return false;
117
  }
118
 
119
- // 簡単なURL形式チェック
120
  try {
121
- const urlObj = new URL(youtubeUrl);
122
- if (!['www.youtube.com', 'youtube.com', 'youtu.be'].includes(urlObj.hostname)) {
123
- throw new Error('Invalid hostname');
 
124
  }
125
- // youtu.be/XXX または youtube.com/watch?v=XXX の形式を期待
126
- if (urlObj.hostname === 'youtu.be' && !urlObj.pathname.substring(1)) {
127
- throw new Error('Missing video ID for youtu.be');
128
  }
129
- if (urlObj.hostname.includes('youtube.com') && urlObj.pathname === '/watch' && !urlObj.searchParams.get('v')) {
130
- throw new Error('Missing video ID for youtube.com/watch');
 
 
 
 
 
 
131
  }
132
- if (urlObj.hostname.includes('youtube.com') && urlObj.pathname.startsWith('/shorts/') && !urlObj.pathname.substring(8)) {
133
- throw new Error('Missing video ID for youtube.com/shorts');
134
- }
135
-
136
  } catch (e) {
137
- displayErrorMessage('有効なYouTube動画リンクを入力してください。(例: https://www.youtube.com/watch?v=...)');
138
- return false;
 
 
139
  }
140
 
141
 
142
  toggleLoading(true, 'generate-button'); // ローディング開始
143
 
144
  try {
 
145
  const response = await fetch('/api/generate', {
146
  method: 'POST',
147
  headers: {
148
  'Content-Type': 'application/json',
149
- 'Accept': 'application/json', // サーバーがJSONを返すことを期待
150
  },
151
- body: JSON.stringify({ url: youtubeUrl }),
152
  });
153
 
154
- const result = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- if (response.ok && result.success && result.data && result.data.id) {
157
- // 成功したら学習画面へ遷移
158
- // alert('生成に成功しました!学習画面に移動します。'); // すぐ遷移するので不要かも
 
 
 
159
  goToLearning(result.data.id);
160
  } else {
161
- // 失敗したらエラーメッセージ表示
162
- console.error('Generation failed:', result);
163
- // サーバーからのメッセージがあれば表示、なければ汎用メッセージ
164
- const serverMessage = result.message || (result.error ? result.error.message : null);
165
- displayErrorMessage(serverMessage || '生成中に不明なエラーが発生しました。');
 
166
  }
167
 
168
  } catch (error) {
 
169
  console.error('Error during generation request:', error);
170
- // ネットワークエラーか、JSONパースエラーか
171
- if (error instanceof SyntaxError) {
172
- displayErrorMessage('サーバーからの応答形式が不正です。');
173
- } else if (error instanceof TypeError) {
174
- displayErrorMessage('通信エラーが発生しました。ネットワーク接続を確認してください。');
175
- }
176
- else {
177
- displayErrorMessage(`通信エラーが発生しました: ${error.message}`);
178
  }
179
  } finally {
180
- toggleLoading(false, 'generate-button'); // ローディング終了
181
  }
182
 
183
- return false; // prevent default form submission
184
  }
185
 
186
 
187
  // --- learning.html 用の処理 ---
188
 
189
  /**
190
- * learning.html の初期化
191
  */
192
  async function initializeLearningScreen() {
193
  console.log('Initializing Learning Screen...');
194
  const params = new URLSearchParams(window.location.search);
195
  const contentId = params.get('id');
196
 
197
- // ★★★ 正解エフェクト要素を取得 ★★★
198
- correctEffect = document.getElementById('correct-effect');
199
 
200
  if (!contentId) {
201
- displayLearningError('コンテンツIDが指定されていません。');
202
  return;
203
  }
204
  console.log('Content ID:', contentId);
@@ -209,61 +283,68 @@ async function initializeLearningScreen() {
209
  const response = await fetch(`/api/learning/${contentId}`);
210
  if (!response.ok) {
211
  // エラーレスポンスがJSON形式でない場合も考慮
212
- let errorMessage = `サーバーからのデータ取得に失敗しました (${response.status})`;
213
  try {
214
  const errorData = await response.json();
215
  errorMessage = errorData.message || errorMessage;
216
  } catch (e) {
217
- // JSONパース失敗時はそのままのエラーメッセージを使う
218
  }
219
  throw new Error(errorMessage);
220
  }
221
 
222
- const result = await response.json();
223
- console.log('Fetched data:', result);
 
224
 
225
- if (result.success && result.data) {
226
- learningData = result.data; // グローバル変数に格納
227
- if (!learningData.items || !Array.isArray(learningData.items) || learningData.items.length === 0) {
228
- throw new Error('学習データが見つからないか、形式が正しくありません。');
229
- }
230
- // タイトルを設定
231
- const titleElement = document.getElementById('learning-title');
232
- if (titleElement) {
233
- titleElement.textContent = learningData.title || '学習コンテンツ';
234
- }
235
 
236
- // 最初のアイテムを表示
237
- currentItemIndex = 0;
238
- displayCurrentItem();
 
 
239
 
 
 
 
 
240
  } else {
241
- throw new Error(result.message || '学習データの読み込みに失敗し��した。');
242
  }
243
 
 
 
 
 
244
  } catch (error) {
 
245
  console.error('Error initializing learning screen:', error);
246
- displayLearningError(`読み込みエラー: ${error.message}`);
 
 
 
 
 
247
  } finally {
248
  toggleLoading(false); // ローディング表示終了
249
  }
250
  }
251
 
252
  /**
253
- * 現在の学習アイテムをカードに表示
254
  */
255
  function displayCurrentItem() {
256
- // ★★★ 最初にエフェクトを隠し、タイマーもクリア ★★★
257
- hideCorrectEffect();
258
- clearTimeout(correctEffectTimeout);
259
 
260
- if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
261
- console.error('Invalid learning data or index');
262
- displayLearningError('学習データを表示できません。');
263
- return;
264
- }
265
-
266
- const item = learningData.items[currentItemIndex];
267
  const cardElement = document.getElementById('learning-card');
268
  const cardTextElement = document.getElementById('card-text');
269
  const answerTextElement = document.getElementById('answer-text');
@@ -271,188 +352,242 @@ function displayCurrentItem() {
271
  const optionsArea = document.getElementById('options-area');
272
  const modeIndicator = document.getElementById('mode-indicator');
273
 
274
- // リセット
275
- cardTextElement.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  answerTextElement.style.display = 'none';
 
277
  tapToShowElement.style.display = 'none';
278
- optionsArea.innerHTML = '';
279
- optionsArea.style.display = 'none'; // デフォルトは非表示
280
  modeIndicator.classList.remove('loading'); // ローディングクラス除去
281
 
282
- if (item.type === 'question' && item.question && item.answer) { // question と answer が存在するかチェック
 
 
283
  currentMode = 'quiz';
284
  modeIndicator.textContent = 'クイズモード';
285
- cardTextElement.textContent = item.question; // question プロパティを使用
286
- answerTextElement.textContent = `答え: ${item.answer}`; // 答えを事前に設定(非表示)
287
- tapToShowElement.style.display = 'block'; // タップして表示を表示
288
 
289
  // 選択肢ボタンを生成
290
  if (item.options && Array.isArray(item.options) && item.options.length > 0) {
291
  optionsArea.style.display = 'block'; // 選択肢があれば表示
292
- // 選択肢をシャッフルする場合(任意)
293
- // const shuffledOptions = [...item.options].sort(() => Math.random() - 0.5);
294
  const optionsToDisplay = item.options; // シャッフルしない場合
295
 
296
  optionsToDisplay.forEach(option => {
297
  const button = document.createElement('button');
298
  button.classList.add('option-button');
299
  button.textContent = option;
300
- // ★★★ 選択肢クリック時の動作を変更 ★★★
301
- button.onclick = () => handleOptionClick(option); // クリックされた選択肢を渡す
302
  optionsArea.appendChild(button);
303
  });
 
304
  } else {
305
- console.warn(`Item ${currentItemIndex} is a question but has no options.`);
306
- // 選択肢がないクイズの場合の表示(例:自由記述?)
307
- tapToShowElement.style.display = 'block'; // 答え表示はできるようにしておく
308
  }
309
- // カード自体のクリックでも解答表示できるようにする
310
- cardElement.onclick = revealAnswer;
311
- tapToShowElement.onclick = revealAnswer;
312
 
313
- } else if (item.type === 'summary' && item.text) { // summary と text が存在するかチェック
314
  currentMode = 'summary';
315
  modeIndicator.textContent = '要約モード';
316
- // 改行を<br>に変換し、安全にHTMLとして設定(XSS対策のためtextContent推奨だが、改行表示のためにinnerHTMLを使用)
317
- // サニタイズが必要な場合はライブラリ(DOMPurifyなど)を使うこと
318
  cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
319
 
320
- // 要約モードでは答えも選択肢も不要、カードクリックも不要
321
  cardElement.onclick = null;
322
  tapToShowElement.style.display = 'none';
 
 
323
  } else {
324
- console.warn('Unknown or invalid item type:', item.type, item);
325
- modeIndicator.textContent = '不明なデータ';
326
- cardTextElement.textContent = `[データ形式エラー] ${item.text || item.question || '内容なし'}`;
327
- cardElement.onclick = null; // クリック動作無効
 
 
 
328
  tapToShowElement.style.display = 'none';
 
329
  }
330
 
331
- updatePagination();
332
  }
333
 
334
- // ★★★ 選択肢クリック時の処理を追加 ★★★
 
 
 
335
  function handleOptionClick(selectedOption) {
336
- if (currentMode !== 'quiz') return; // クイズモード以外では何もしない
 
337
 
 
 
 
 
 
 
 
 
338
  const currentItem = learningData.items[currentItemIndex];
339
  const correctAnswer = currentItem.answer;
340
  const isCorrect = selectedOption === correctAnswer;
341
 
342
- // --- 正解だった場合の処理 ---
343
  if (isCorrect) {
344
- console.log("正解!");
345
- showCorrectEffect(); // ★★★ 正解エフェクトを表示 ★★★
346
  } else {
347
- console.log("不正解...");
348
- // 不正解時のエフェクトなどが必要ならここに追加
349
  }
350
 
351
- // 選択肢ボタンの状態更新と解答表示
352
- revealAnswer(selectedOption); // 選んだ選択肢を渡してスタイルを設定
353
  }
354
 
355
 
356
  /**
357
- * クイズの解答を表示する(選択肢クリックまたはカードタップ時)
358
- * @param {string|null} selectedOption - ユーザーが選択した選択肢 (選択肢クリック時のみ渡される)
359
  */
360
  function revealAnswer(selectedOption = null) {
361
- // クイズモードの場合のみ動作
362
- if (currentMode === 'quiz') {
363
- const answerTextElement = document.getElementById('answer-text');
364
- const tapToShowElement = document.getElementById('tap-to-show');
365
- const optionsArea = document.getElementById('options-area');
366
- const cardElement = document.getElementById('learning-card');
367
-
368
- // 既に表示済みなら何もしない(二重実行防止)
369
- if (answerTextElement && answerTextElement.style.display === 'block') {
370
- return;
371
- }
372
 
 
 
 
 
373
 
374
- if (answerTextElement) {
375
- answerTextElement.style.display = 'block'; // 答えを表示
376
- }
377
- if (tapToShowElement) {
378
- tapToShowElement.style.display = 'none'; // 「タップして表示」を隠す
379
- }
380
- // カード自体のクリックイベントを解除(解答表示後は不要)
381
- if (cardElement) {
382
- cardElement.onclick = null;
383
- }
 
 
384
 
385
- // 選択肢ボタンを正解・不正解で色付け&無効化
386
- if (optionsArea && learningData && learningData.items[currentItemIndex]) {
387
- const correctAnswer = learningData.items[currentItemIndex].answer;
388
- const buttons = optionsArea.getElementsByTagName('button');
389
- for (let btn of buttons) {
390
- btn.disabled = true; // すべてのボタンを無効化
391
- btn.onclick = null; // クリックイベントも削除
392
-
393
- if (btn.textContent === correctAnswer) {
394
- btn.classList.add('correct'); // 正解ボタン
395
- } else if (btn.textContent === selectedOption) {
396
- btn.classList.add('incorrect'); // 選択された不正解ボタン
397
- } else {
398
- btn.classList.add('other-disabled'); // 選ばれなかった他の選択肢
399
- }
 
 
 
 
 
400
  }
401
- }
402
  }
403
  }
404
 
405
 
406
  /**
407
- * 次のアイテムへ移動
408
  */
409
  function goToNext() {
410
- if (learningData && currentItemIndex < learningData.items.length - 1) {
 
411
  currentItemIndex++;
412
- displayCurrentItem();
 
 
 
 
 
 
413
  }
414
  }
415
 
416
  /**
417
- * 前のアイテムへ移動
418
  */
419
  function goToPrev() {
420
- if (learningData && currentItemIndex > 0) {
 
421
  currentItemIndex--;
422
- displayCurrentItem();
 
 
423
  }
424
  }
425
 
426
  /**
427
- * ページネーション表示を更新
428
  */
429
  function updatePagination() {
430
  const pageInfo = document.getElementById('page-info');
431
  const prevButton = document.getElementById('prev-button');
432
  const nextButton = document.getElementById('next-button');
433
 
434
- if (pageInfo && prevButton && nextButton && learningData && learningData.items) {
 
 
 
 
 
 
 
435
  const totalItems = learningData.items.length;
436
- // totalItemsが0の場合のエラーを防ぐ
437
- if (totalItems > 0) {
438
- pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
439
- prevButton.disabled = currentItemIndex === 0;
440
- nextButton.disabled = currentItemIndex === totalItems - 1;
441
- } else {
442
- pageInfo.textContent = `0 / 0`;
443
- prevButton.disabled = true;
444
- nextButton.disabled = true;
445
- }
446
  } else {
447
- // 要素がないかデータがない場合
448
- if(pageInfo) pageInfo.textContent = '- / -';
449
- if(prevButton) prevButton.disabled = true;
450
- if(nextButton) nextButton.disabled = true;
451
  }
452
  }
453
 
454
  /**
455
- * learning.html でエラーを表示
 
456
  */
457
  function displayLearningError(message) {
458
  const cardElement = document.getElementById('learning-card');
@@ -462,177 +597,240 @@ function displayLearningError(message) {
462
  const modeIndicator = document.getElementById('mode-indicator');
463
  const tapToShow = document.getElementById('tap-to-show');
464
 
 
465
  if (titleElement) titleElement.textContent = 'エラー';
466
- if (modeIndicator) modeIndicator.textContent = 'エラー';
467
  if (cardElement) {
 
468
  cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center; padding: 20px;">${message}</p>`;
469
- cardElement.onclick = null; // クリック無効
470
  }
471
- if (paginationElement) paginationElement.style.display = 'none';
472
- if (optionsArea) optionsArea.innerHTML = ''; // オプションクリア
473
- if (optionsArea) optionsArea.style.display = 'none';
474
- if (tapToShow) tapToShow.style.display = 'none';
 
 
 
 
 
475
  }
476
 
477
- // --- ★★★ 正解エフェクト表示関数を追加 ★★★ ---
 
 
478
  function showCorrectEffect() {
479
  if (correctEffect) {
480
- // 既存のタイムアウトがあればクリア
481
- clearTimeout(correctEffectTimeout);
482
-
483
- correctEffect.classList.add('show'); // 表示用クラスを追加
484
 
485
- // 1秒後 (1000ms) に非表示処理を開始
486
  correctEffectTimeout = setTimeout(() => {
487
  hideCorrectEffect();
488
- }, 1000); // 表示時間はここで調整 (ミリ秒)
 
 
489
  }
490
  }
491
 
492
- // --- ★★★ 正解エフェクト非表示関数を追加 ★★★ ---
 
 
493
  function hideCorrectEffect() {
494
  if (correctEffect && correctEffect.classList.contains('show')) {
495
- correctEffect.classList.remove('show');
496
- // アニメーション完了後に display: none に戻したい場合は transitionend イベントを使う
497
- // correctEffect.addEventListener('transitionend', () => {
498
- // correctEffect.style.display = 'none';
499
- // }, { once: true }); // 一度だけ実行
500
- // ただし、クラスの付け外しだけで opacity と transform を制御する方がシンプル
501
  }
 
 
502
  }
503
 
504
 
505
  // --- settings.html 用の処理 ---
506
 
507
  /**
508
- * トグルスイッチの変更ハンドラ
 
 
509
  */
510
  function handleToggleChange(checkbox, type) {
511
- console.log(`Toggle changed for ${type}: ${checkbox.checked}`);
 
 
512
  if (type === 'dark') {
513
- document.body.classList.toggle('dark-mode', checkbox.checked);
 
 
514
  try {
515
- localStorage.setItem('darkModeEnabled', checkbox.checked);
 
516
  } catch (e) {
517
- console.warn('Could not save dark mode preference to localStorage.');
 
 
518
  }
 
 
 
 
 
519
  }
520
- // 他のトグル(例: プッシュ通知)の処理もここに追加
521
  }
522
 
523
  /**
524
- * ログアウトボタンの処理
525
  */
526
  function handleLogout() {
527
- console.log("Logout clicked");
528
- // TODO: 実際のログアウト処理(API呼び出し、セッションクリアなど)
529
- alert("ログアウトしました。(機能は未実装)");
530
- // 必要であればログイン画面などに遷移
531
- // navigateTo('/login');
532
- // 例として入力画面に戻る
533
- goToInput();
 
 
 
534
  }
535
 
536
  /**
537
- * ダークモード設定を読み込んで適用する
538
  */
539
  function applyDarkModePreference() {
540
  try {
 
541
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
542
  document.body.classList.toggle('dark-mode', darkModeEnabled);
543
- // 設定画面のトグルスイッチの状態も合わせる
544
- // DOMContentLoaded より前に実行される場合があるので、
545
- // settings.htmlの初期化時にスイッチの状態を更新する方が確実
546
  } catch (e) {
547
- console.warn('Could not load dark mode preference from localStorage.');
 
548
  }
549
  }
550
 
551
 
552
  // --- ページの初期化処理 ---
553
- const pathname = window.location.pathname; // グローバルスコープで取得
554
 
555
- // ダークモード設定を最初に適用 (FOUC対策)
556
  applyDarkModePreference();
557
 
 
558
  document.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
 
559
 
560
- if (pathname === '/' || pathname === '/input' || pathname === '/input.html') { // input.html のパスパターンを考慮
 
 
 
 
 
 
 
 
 
 
 
561
  const form = document.getElementById('generate-form');
562
  if (form) {
 
563
  form.addEventListener('submit', (event) => {
564
- event.preventDefault();
565
- handleGenerateSubmit();
566
  });
 
 
567
  }
568
- // input.html 専用の初期化があればここに追加
569
- // 例: URLパラメータから初期値を設定するなど
570
  const urlParams = new URLSearchParams(window.location.search);
571
  const initialUrl = urlParams.get('url');
572
  if (initialUrl) {
573
  const urlInput = document.getElementById('youtube-url');
574
  if (urlInput) {
575
  urlInput.value = initialUrl;
 
576
  }
577
  }
578
-
579
- } else if (pathname === '/learning' || pathname === '/learning.html') {
580
- initializeLearningScreen();
581
- } else if (pathname === '/history' || pathname === '/history.html') {
582
- // history.html の初期化(動的にリストを生成する場合など)
583
- console.log("History page loaded.");
584
- // loadHistoryData(); // 仮の関数呼び出し
585
- } else if (pathname === '/settings' || pathname === '/settings.html') {
586
- // settings.html の初期化
587
- console.log("Settings page loaded.");
588
  // ダークモードトグルの状態をlocalStorageに合わせて更新
589
  try {
590
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
591
- const toggle = document.querySelector('input[onchange*="handleToggleChange"][onchange*="dark"]');
 
592
  if (toggle) {
593
  toggle.checked = darkModeEnabled;
 
 
 
594
  }
595
  } catch (e) {
596
- console.warn('Could not set dark mode toggle state.');
597
  }
598
  }
599
 
600
- // フッターナビゲーションのアクティブ状態設定
601
  updateFooterNavActiveState(pathname);
 
 
 
 
602
  });
603
 
604
 
605
  /**
606
- * フッターナビゲーションのアクティブ状態を更新
607
  */
608
  function updateFooterNavActiveState(currentPath) {
609
  const footerNav = document.querySelector('.footer-nav');
610
- if (!footerNav) return;
 
 
 
611
 
612
  const buttons = footerNav.querySelectorAll('button');
 
 
613
  buttons.forEach(button => {
614
- button.classList.remove('active'); // Reset all
615
  const onclickAttr = button.getAttribute('onclick');
616
  if (onclickAttr) {
617
- // パスの末尾が一致するか、またはルートパスの場合を確認
618
  if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) {
619
  button.classList.add('active');
 
620
  } else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) {
621
  button.classList.add('active');
 
622
  } else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) {
623
  button.classList.add('active');
 
624
  }
625
- // learning ページの場合はどのフッターもアクティブにしない (任意)
626
- else if ((currentPath.endsWith('/learning') || currentPath.endsWith('/learning.html'))) {
627
- // 何もしない(アクティブにならない)
628
- }
629
  }
630
  });
 
 
 
631
  }
632
 
633
 
634
- // デバッグ用に一部関数をグローバルスコープに公開(開発中のみ推奨)
635
- // 本番環境では削除またはコメントアウトすること
636
  window.debug = {
637
  navigateTo,
638
  goToInput,
@@ -640,18 +838,21 @@ window.debug = {
640
  goToSettings,
641
  goToLearning,
642
  openMenu,
 
643
  handleGenerateSubmit,
644
  initializeLearningScreen,
645
- handleOptionClick, // handleOptionClick を公開
646
  revealAnswer,
647
  goToNext,
648
  goToPrev,
649
  handleToggleChange,
650
  handleLogout,
651
- showCorrectEffect, // デバッグ用にエフェクト表示関数も公開
652
- hideCorrectEffect, // デバッグ用にエフェクト非表示関数も公開
653
  learningData, // 現在の学習データ確認用
654
- currentItemIndex // 現在のインデックス確認用
 
 
655
  };
656
 
657
  // --- END OF FILE script.js ---
 
3
  "use strict"; // より厳格なエラーチェック
4
 
5
  // --- グローバル変数 ---
6
+ let learningData = null; // 学習データ (learning.html用) - { title: '...', items: [...] } の形式を想定
7
  let currentItemIndex = 0; // 現在表示中のアイテムインデックス (learning.html用)
8
  let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary' (learning.html用)
9
  let correctEffectTimeout; // 正解エフェクト非表示用のタイマーID (learning.html用)
10
  let correctEffect = null; // 正解エフェクト要素 (learning.html用)
11
+ let sideMenu = null; // サイドメニュー要素
12
+ let menuOverlay = null; // メニューオーバーレイ要素
13
 
14
  // --- 共通関数 ---
15
 
16
  /**
17
+ * 指定されたURLに遷移します。遷移前にメニューを閉じます。
18
  * @param {string} url - 遷移先のURL
19
  */
20
  function navigateTo(url) {
21
+ closeMenu(); // 遷移前にメニューを閉じる
22
+ // 少し遅延させてから遷移する(メニューが閉じるアニメーションを見せるため、任意)
23
+ setTimeout(() => {
24
+ window.location.href = url;
25
+ }, 100); // 100ミリ秒後に遷移
26
  }
27
 
28
  /**
29
+ * サイドメニューを開きます。
 
30
  */
31
  function openMenu() {
32
+ console.log("Opening menu...");
33
+ if (sideMenu && menuOverlay) {
34
+ sideMenu.classList.add('open');
35
+ menuOverlay.classList.add('open');
36
+ document.body.classList.add('menu-open'); // 背景スクロール禁止
37
+ } else {
38
+ console.error("Side menu or overlay element not found. Cannot open menu.");
39
+ }
40
+ }
41
+
42
+ /**
43
+ * サイドメニューを閉じます。
44
+ */
45
+ function closeMenu() {
46
+ // console.log("Closing menu..."); // デバッグ時以外は不要かも
47
+ if (sideMenu && menuOverlay) {
48
+ sideMenu.classList.remove('open');
49
+ menuOverlay.classList.remove('open');
50
+ document.body.classList.remove('menu-open'); // 背景スクロール許可
51
+ }
52
+ // メニュー要素が見つからない場合のエラーログはここでは不要
53
  }
54
 
55
+
56
  /**
57
  * ローディングスピナーを表示/非表示します。
58
  * @param {boolean} show - trueで表示、falseで非表示
59
  * @param {string} buttonId - 操作対象のボタンID (input.html用)
60
  */
61
  function toggleLoading(show, buttonId = 'generate-button') {
62
+ const targetButton = document.getElementById(buttonId);
 
63
 
64
+ // input.html generate-button の処理
65
+ if (targetButton && buttonId === 'generate-button') {
66
+ const spinner = targetButton.querySelector('.loading-spinner');
67
+ const buttonText = targetButton.querySelector('.button-text');
68
 
69
+ if (show) {
70
+ targetButton.disabled = true;
71
+ if (spinner) spinner.style.display = 'inline-block';
72
+ if (buttonText) buttonText.textContent = '生成中...';
73
+ } else {
74
+ targetButton.disabled = false;
75
+ if (spinner) spinner.style.display = 'none';
76
+ if (buttonText) buttonText.textContent = '生成する'; // 元のテキストに戻す
77
+ }
78
  }
79
 
80
+ // learning.html 用の汎用ローディング表示 (mode-indicatorなどを利用)
81
  const loadingIndicator = document.getElementById('mode-indicator');
82
  const cardElement = document.getElementById('learning-card');
83
  const paginationElement = document.querySelector('.pagination');
84
  const optionsArea = document.getElementById('options-area');
85
+ const tapToShowElement = document.getElementById('tap-to-show'); // 追加
86
+
87
+ // 現在のパスがlearningページか確認
88
+ const currentPathname = window.location.pathname;
89
+ if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) {
90
+ if (show) {
91
+ if (loadingIndicator) {
92
+ loadingIndicator.textContent = '読み込み中...';
93
+ loadingIndicator.classList.add('loading');
94
+ }
95
+ if (cardElement) cardElement.style.opacity = '0.5';
96
+ if (paginationElement) paginationElement.style.display = 'none';
97
+ if (optionsArea) optionsArea.style.display = 'none';
98
+ if (tapToShowElement) tapToShowElement.style.display = 'none'; // ローディング中は非表示
99
+ } else {
100
+ // ローディング終了時の表示は displayCurrentItem で制御されるため、
101
+ // ここではローディングインジケータのテキストクリア程度で良い
102
+ if (loadingIndicator) {
103
+ // loadingIndicator.textContent = ''; // displayCurrentItemでモードが表示される
104
+ loadingIndicator.classList.remove('loading');
105
+ }
106
+ if (cardElement) cardElement.style.opacity = '1';
107
+ // pagination, optionsArea, tapToShowElement の表示は displayCurrentItem に任せる
108
+ }
109
  }
110
  }
111
 
 
118
  const errorElement = document.getElementById(elementId);
119
  if (errorElement) {
120
  errorElement.textContent = message;
121
+ // メッセージがあれば表示、なければ非表示 (display: block/none)
122
  errorElement.style.display = message ? 'block' : 'none';
123
+ } else {
124
+ // エラー表示要素自体が見つからない場合
125
+ if (message) { // メッセージがある場合のみコンソールにエラー表示
126
+ console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`);
127
+ }
128
  }
129
  }
130
 
131
  // --- 画面遷移用関数 ---
132
  function goToInput() {
133
+ navigateTo('/'); // input.html はルートパスを想定
134
  }
135
 
136
  function goToHistory() {
 
143
 
144
  function goToLearning(contentId) {
145
  if (contentId) {
146
+ // navigateTo内でメニューは閉じられる
147
  navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
148
  } else {
149
  console.error('goToLearning requires a content ID.');
150
+ // ユーザーへのフィードバック
151
+ alert('学習コンテンツのIDが見つかりません。履歴画面から再度お試しください。');
152
  }
153
  }
154
 
 
161
  async function handleGenerateSubmit() {
162
  const urlInput = document.getElementById('youtube-url');
163
  const youtubeUrl = urlInput.value.trim();
164
+ const errorMsgElementId = 'error-message'; // エラーメッセージ表示用ID
165
+ displayErrorMessage('', errorMsgElementId); // 前のエラーメッセージをクリア
166
 
167
  if (!youtubeUrl) {
168
+ displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId);
169
+ return false; // 送信中断
170
  }
171
 
172
+ // 簡単なURL形式チェックと YouTube ドメイン検証
173
  try {
174
+ const urlObj = new URL(youtubeUrl); // URLオブジェクト生成試行
175
+ const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be'];
176
+ if (!validHostnames.includes(urlObj.hostname)) {
177
+ throw new Error('Invalid hostname'); // YouTube以外のドメイン
178
  }
179
+ // さらに詳細なパスチェック(例)
180
+ if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) {
181
+ throw new Error('Missing video ID for youtu.be'); // youtu.be/ の後にIDがない
182
  }
183
+ if (urlObj.hostname.includes('youtube.com')) {
184
+ if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) {
185
+ throw new Error('Missing video ID parameter (v=...) for youtube.com/watch'); // vパラメータがない
186
+ }
187
+ if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) {
188
+ throw new Error('Missing video ID for youtube.com/shorts/'); // shorts/ の後にIDがない
189
+ }
190
+ // 他の有効な形式があれば追加 (例: /live/ など)
191
  }
 
 
 
 
192
  } catch (e) {
193
+ // URLパースエラーまたは上記バリデーションエラー
194
+ console.warn("Invalid URL format:", e.message);
195
+ displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId);
196
+ return false; // 送信中断
197
  }
198
 
199
 
200
  toggleLoading(true, 'generate-button'); // ローディング開始
201
 
202
  try {
203
+ // APIエンドポイント '/api/generate' へPOSTリクエスト
204
  const response = await fetch('/api/generate', {
205
  method: 'POST',
206
  headers: {
207
  'Content-Type': 'application/json',
208
+ 'Accept': 'application/json', // サーバーからのJSON応答を期待
209
  },
210
+ body: JSON.stringify({ url: youtubeUrl }), // URLをJSON形式で送信
211
  });
212
 
213
+ // レスポンスボディをJSONとしてパース試行
214
+ let result;
215
+ try {
216
+ result = await response.json();
217
+ } catch (jsonError) {
218
+ // JSONパース失敗
219
+ console.error('Failed to parse JSON response:', jsonError);
220
+ // サーバーエラーの可能性が高いが、レスポンスステータスも確認
221
+ if (!response.ok) {
222
+ throw new Error(`サーバーエラー (${response.status})。応答形式が不正です。`);
223
+ } else {
224
+ // 成功ステータスなのにJSONでない場合(通常はありえない)
225
+ throw new Error('サーバーからの応答形式が予期せぬ形式です。');
226
+ }
227
+ }
228
 
229
+
230
+ // ★★★ サーバーが返すJSON構造に合わせて修正 ★★★
231
+ // サーバーが { success: true, data: { id: '...' } } のような形式を返す想定
232
+ if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) {
233
+ // 生成成功: 取得したIDを使って学習画面へ遷移
234
+ console.log("Generation successful, navigating to learning page with ID:", result.data.id);
235
  goToLearning(result.data.id);
236
  } else {
237
+ // APIがエラーを返した場合、または期待する構造でなかった場合
238
+ console.error('Generation API call failed or returned unexpected structure:', result);
239
+ const serverMessage = (result && typeof result === 'object' && result.message) || // resultがオブジェクトでmessageがあればそれを表示
240
+ (result && typeof result === 'object' && result.error && result.error.message) || // error.messageがあればそれを表示
241
+ (response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`); // それ以外の場合
242
+ displayErrorMessage(serverMessage, errorMsgElementId);
243
  }
244
 
245
  } catch (error) {
246
+ // fetch自体が失敗した場合(ネットワークエラーなど)または上記のthrow new Error
247
  console.error('Error during generation request:', error);
248
+ // ユーザーフレンドリーなメッセージ表示
249
+ if (error.message.includes('Failed to fetch')) {
250
+ displayErrorMessage('サーバーに接続できませんでした。ネットワーク接続を確認してください。', errorMsgElementId);
251
+ } else {
252
+ displayErrorMessage(`エラーが発生しました: ${error.message}`, errorMsgElementId);
 
 
 
253
  }
254
  } finally {
255
+ toggleLoading(false, 'generate-button'); // ローディング終了(成功・失敗問わず)
256
  }
257
 
258
+ return false; // formのデフォルト送信を常にキャンセル
259
  }
260
 
261
 
262
  // --- learning.html 用の処理 ---
263
 
264
  /**
265
+ * learning.html の初期化: コンテンツデータを取得し、最初のアイテムを表示
266
  */
267
  async function initializeLearningScreen() {
268
  console.log('Initializing Learning Screen...');
269
  const params = new URLSearchParams(window.location.search);
270
  const contentId = params.get('id');
271
 
272
+ // 正解エフェクト要素はDOMContentLoadedで取得済みのはず
 
273
 
274
  if (!contentId) {
275
+ displayLearningError('学習コンテンツのIDが見つかりません。');
276
  return;
277
  }
278
  console.log('Content ID:', contentId);
 
283
  const response = await fetch(`/api/learning/${contentId}`);
284
  if (!response.ok) {
285
  // エラーレスポンスがJSON形式でない場合も考慮
286
+ let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`;
287
  try {
288
  const errorData = await response.json();
289
  errorMessage = errorData.message || errorMessage;
290
  } catch (e) {
291
+ console.warn('Failed to parse error response as JSON.');
292
  }
293
  throw new Error(errorMessage);
294
  }
295
 
296
+ // ★★★ サーバーからの応答が配列であると想定して処理 ★★★
297
+ const itemsArray = await response.json();
298
+ console.log('Fetched data (items array):', itemsArray);
299
 
300
+ // 配列であるか、空でないかをチェック
301
+ if (!Array.isArray(itemsArray)) {
302
+ throw new Error('サーバーからのデータ形式が不正です (配列ではありません)。');
303
+ }
304
+ if (itemsArray.length === 0) {
305
+ throw new Error('学習データが見つかりませんでした (空の配列)。');
306
+ }
 
 
 
307
 
308
+ // learningData オブジェクトを構築
309
+ learningData = {
310
+ title: `学習セット (${contentId})`, // タイトルは固定文字列またはcontentIdから生成
311
+ items: itemsArray // 取得した配列を items プロパティに設定
312
+ };
313
 
314
+ // タイトルを設定 (learningData.title を使用)
315
+ const titleElement = document.getElementById('learning-title');
316
+ if (titleElement) {
317
+ titleElement.textContent = learningData.title || '学習セット'; // デフォルトタイトル
318
  } else {
319
+ console.warn("Title element ('learning-title') not found.");
320
  }
321
 
322
+ // 最初のアイテムを表示
323
+ currentItemIndex = 0;
324
+ displayCurrentItem();
325
+
326
  } catch (error) {
327
+ // fetchエラーまたは上記のthrow new Error
328
  console.error('Error initializing learning screen:', error);
329
+ // JSONパースエラーの場合のメッセージを改善
330
+ if (error instanceof SyntaxError) {
331
+ displayLearningError(`サーバー応答の解析エラー: ${error.message}`);
332
+ } else {
333
+ displayLearningError(`読み込みエラー: ${error.message}`);
334
+ }
335
  } finally {
336
  toggleLoading(false); // ローディング表示終了
337
  }
338
  }
339
 
340
  /**
341
+ * 現在の学習アイテムをカードに表示 (クイズ or 要約)
342
  */
343
  function displayCurrentItem() {
344
+ hideCorrectEffect(); // 前のエフェクトを隠す
345
+ clearTimeout(correctEffectTimeout); // タイマーもクリア
 
346
 
347
+ // 要素取得 (毎回取得する方が確実)
 
 
 
 
 
 
348
  const cardElement = document.getElementById('learning-card');
349
  const cardTextElement = document.getElementById('card-text');
350
  const answerTextElement = document.getElementById('answer-text');
 
352
  const optionsArea = document.getElementById('options-area');
353
  const modeIndicator = document.getElementById('mode-indicator');
354
 
355
+ // 要素が存在しない場合はエラーを出して終了
356
+ if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) {
357
+ console.error("One or more required learning elements are missing from the DOM.");
358
+ displayLearningError("画面の表示に必要な要素が見つかりません。");
359
+ return;
360
+ }
361
+
362
+ // データ存在チェック (learningData と learningData.items をチェック)
363
+ if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
364
+ console.error('Invalid learning data or index:', learningData, currentItemIndex);
365
+ displayLearningError('表示する学習データが見つかりません。');
366
+ return;
367
+ }
368
+
369
+ // ★★★ learningData.items から現在のアイテムを取得 ★★★
370
+ const item = learningData.items[currentItemIndex];
371
+
372
+ // 表示内容リセット
373
+ cardTextElement.innerHTML = ''; // textContentではなくinnerHTMLでリセット
374
  answerTextElement.style.display = 'none';
375
+ answerTextElement.textContent = ''; // 内容もクリア
376
  tapToShowElement.style.display = 'none';
377
+ optionsArea.innerHTML = ''; // 選択肢エリアクリア
378
+ optionsArea.style.display = 'none'; // 一旦非表示
379
  modeIndicator.classList.remove('loading'); // ローディングクラス除去
380
 
381
+ // itemタイプに基づいて表示を分岐
382
+ // ★★★ 問題文のキーを 'text' に修正 ★★★
383
+ if (item.type === 'question' && item.text && item.answer) { // `item.question` -> `item.text` に変更
384
  currentMode = 'quiz';
385
  modeIndicator.textContent = 'クイズモード';
386
+ // ★★★ 問題文には item.text を使用 ★★★
387
+ cardTextElement.textContent = item.text; // 問題文はtextContentで安全に設定
388
+ answerTextElement.textContent = `答え: ${item.answer}`; // 答えは事前に設定(非表示)
389
 
390
  // 選択肢ボタンを生成
391
  if (item.options && Array.isArray(item.options) && item.options.length > 0) {
392
  optionsArea.style.display = 'block'; // 選択肢があれば表示
 
 
393
  const optionsToDisplay = item.options; // シャッフルしない場合
394
 
395
  optionsToDisplay.forEach(option => {
396
  const button = document.createElement('button');
397
  button.classList.add('option-button');
398
  button.textContent = option;
399
+ // クリックしたら handleOptionClick を呼ぶ
400
+ button.onclick = () => handleOptionClick(option);
401
  optionsArea.appendChild(button);
402
  });
403
+ tapToShowElement.style.display = 'block'; // 選択肢がある場合もタップ表示は有効
404
  } else {
405
+ // 選択肢がないクイズの場合
406
+ console.warn(`Quiz item ${currentItemIndex} has no options.`);
407
+ tapToShowElement.style.display = 'block'; // 答え表示は可能にする
408
  }
409
+ // カード自体 or タップ表示テキストのクリックで解答表示
410
+ cardElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
411
+ tapToShowElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
412
 
413
+ } else if (item.type === 'summary' && item.text) {
414
  currentMode = 'summary';
415
  modeIndicator.textContent = '要約モード';
416
+ // 改行(\n)を<br>に置換して表示 (innerHTMLを使うので注意)
 
417
  cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
418
 
419
+ // 要約モードではクリックイベント不要
420
  cardElement.onclick = null;
421
  tapToShowElement.style.display = 'none';
422
+ optionsArea.style.display = 'none'; // 念のため
423
+
424
  } else {
425
+ // 不明なタイプまたは必要なデータ (text answer) が欠けている場合
426
+ console.warn('Unknown or invalid item type/data:', item);
427
+ currentMode = 'unknown';
428
+ modeIndicator.textContent = 'データエラー';
429
+ // ★★★ `item.text` を表示試行するように変更 ★★★
430
+ cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`;
431
+ cardElement.onclick = null;
432
  tapToShowElement.style.display = 'none';
433
+ optionsArea.style.display = 'none';
434
  }
435
 
436
+ updatePagination(); // ページネーション表示を更新
437
  }
438
 
439
+ /**
440
+ * クイズの選択肢がクリックされたときの処理
441
+ * @param {string} selectedOption - ユーザーが選択した選択肢のテキスト
442
+ */
443
  function handleOptionClick(selectedOption) {
444
+ // ★★★ learningData.items の存在もチェック ★★★
445
+ if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
446
 
447
+ // 解答表示がすでに行われていないかチェック(二重処理防止)
448
+ const answerTextElement = document.getElementById('answer-text');
449
+ if (answerTextElement && answerTextElement.style.display === 'block') {
450
+ console.log('Answer already revealed, ignoring option click.');
451
+ return;
452
+ }
453
+
454
+ // ★★★ learningData.items から現在のアイテムを取得 ★★★
455
  const currentItem = learningData.items[currentItemIndex];
456
  const correctAnswer = currentItem.answer;
457
  const isCorrect = selectedOption === correctAnswer;
458
 
 
459
  if (isCorrect) {
460
+ console.log("Correct!");
461
+ showCorrectEffect(); // ★★★ 正解エフェクト表示 ★★★
462
  } else {
463
+ console.log("Incorrect...");
464
+ // 不正解時のフィードバック(例: ボタンを赤くするなど)はrevealAnswerで行う
465
  }
466
 
467
+ // 選択された選択肢を引数に渡して、解答表示とボタンの状態更新を行う
468
+ revealAnswer(selectedOption);
469
  }
470
 
471
 
472
  /**
473
+ * クイズの解答を表示し、選択肢ボタンの状態を更新する
474
+ * @param {string|null} [selectedOption=null] - ユーザーが選択した選択肢。nullの場合はカードタップ等による表示。
475
  */
476
  function revealAnswer(selectedOption = null) {
477
+ // ★★★ learningData.items の存在もチェック ★★★
478
+ if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
479
+
480
+ const answerTextElement = document.getElementById('answer-text');
481
+ const tapToShowElement = document.getElementById('tap-to-show');
482
+ const optionsArea = document.getElementById('options-area');
483
+ const cardElement = document.getElementById('learning-card'); // カード要素も取得
 
 
 
 
484
 
485
+ // 既に表示済みなら何もしない
486
+ if (answerTextElement && answerTextElement.style.display === 'block') {
487
+ return;
488
+ }
489
 
490
+ // 解答テキストを表示
491
+ if (answerTextElement) {
492
+ answerTextElement.style.display = 'block';
493
+ }
494
+ // 「タップして表示」を隠す
495
+ if (tapToShowElement) {
496
+ tapToShowElement.style.display = 'none';
497
+ }
498
+ // カード自体のクリックイベントを無効化(解答表示後は不要)
499
+ if (cardElement) {
500
+ cardElement.onclick = null;
501
+ }
502
 
503
+ // 選択肢ボタンがあれば状態を更新
504
+ if (optionsArea) {
505
+ // ★★★ learningData.items から現在のアイテムを取得 ★★★
506
+ const correctAnswer = learningData.items[currentItemIndex].answer;
507
+ const buttons = optionsArea.querySelectorAll('.option-button'); // querySelectorAll推奨
508
+
509
+ buttons.forEach(button => {
510
+ button.disabled = true; // 全てのボタンを無効化
511
+ button.onclick = null; // クリックイベント解除
512
+
513
+ const buttonText = button.textContent;
514
+
515
+ if (buttonText === correctAnswer) {
516
+ button.classList.add('correct'); // 正解ボタンにクラス付与
517
+ } else if (buttonText === selectedOption) {
518
+ // 不正解で、かつユーザーが選択したボタン
519
+ button.classList.add('incorrect');
520
+ } else {
521
+ // 正解でもなく、ユーザーが選択したものでもないボタン
522
+ button.classList.add('other-disabled'); // 他の選択肢用のスタイル
523
  }
524
+ });
525
  }
526
  }
527
 
528
 
529
  /**
530
+ * 次の学習アイテムへ移動
531
  */
532
  function goToNext() {
533
+ // ★★★ learningData.items の存在もチェック ★★★
534
+ if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) {
535
  currentItemIndex++;
536
+ displayCurrentItem(); // 次のアイテムを表示
537
+ } else {
538
+ console.log("Already at the last item or no data.");
539
+ // 最後のアイテムの場合、完了メッセージなどを表示しても良い
540
+ if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) {
541
+ alert("学習セットが完了しました!");
542
+ }
543
  }
544
  }
545
 
546
  /**
547
+ * 前の学習アイテムへ移動
548
  */
549
  function goToPrev() {
550
+ // ★★★ learningData.items の存在もチェック ★★★
551
+ if (learningData && learningData.items && currentItemIndex > 0) {
552
  currentItemIndex--;
553
+ displayCurrentItem(); // 前のアイテムを表示
554
+ } else {
555
+ console.log("Already at the first item or no data.");
556
  }
557
  }
558
 
559
  /**
560
+ * ページネーション(ページ番号とボタンの状態)を更新
561
  */
562
  function updatePagination() {
563
  const pageInfo = document.getElementById('page-info');
564
  const prevButton = document.getElementById('prev-button');
565
  const nextButton = document.getElementById('next-button');
566
 
567
+ // 要素の存在チェック
568
+ if (!pageInfo || !prevButton || !nextButton) {
569
+ console.warn("Pagination elements not found.");
570
+ return;
571
+ }
572
+
573
+ // ★★★ learningData.items の存在をチェック ★★★
574
+ if (learningData && learningData.items && learningData.items.length > 0) {
575
  const totalItems = learningData.items.length;
576
+ pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
577
+ // ボタンの有効/無効を設定
578
+ prevButton.disabled = currentItemIndex === 0;
579
+ nextButton.disabled = currentItemIndex === totalItems - 1;
 
 
 
 
 
 
580
  } else {
581
+ // データがない場合や空の場合
582
+ pageInfo.textContent = '0 / 0';
583
+ prevButton.disabled = true;
584
+ nextButton.disabled = true;
585
  }
586
  }
587
 
588
  /**
589
+ * learning.html でエラーが発生した場合の表示処理
590
+ * @param {string} message - 表示するエラーメッセージ
591
  */
592
  function displayLearningError(message) {
593
  const cardElement = document.getElementById('learning-card');
 
597
  const modeIndicator = document.getElementById('mode-indicator');
598
  const tapToShow = document.getElementById('tap-to-show');
599
 
600
+ // 各要素が存在すればエラー表示に切り替える
601
  if (titleElement) titleElement.textContent = 'エラー';
602
+ if (modeIndicator) modeIndicator.textContent = 'エラー発生';
603
  if (cardElement) {
604
+ // カードの内容をエラーメッセージで置き換え
605
  cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center; padding: 20px;">${message}</p>`;
606
+ cardElement.onclick = null; // クリックイベント解除
607
  }
608
+ if (paginationElement) paginationElement.style.display = 'none'; // ページネーション非表示
609
+ if (optionsArea) {
610
+ optionsArea.innerHTML = ''; // 選択肢クリア
611
+ optionsArea.style.display = 'none';
612
+ }
613
+ if (tapToShow) tapToShow.style.display = 'none'; // タップ表示も隠す
614
+
615
+ // エラー時はローディング表示も確実に解除
616
+ toggleLoading(false);
617
  }
618
 
619
+ /**
620
+ * 正解時に「〇」エフェクトを表示する
621
+ */
622
  function showCorrectEffect() {
623
  if (correctEffect) {
624
+ clearTimeout(correctEffectTimeout); // 既存のタイマーをクリア
625
+ correctEffect.classList.add('show'); // 表示クラスを追加
 
 
626
 
627
+ // 指定時間後 (例: 1秒) に非表示処理を開始
628
  correctEffectTimeout = setTimeout(() => {
629
  hideCorrectEffect();
630
+ }, 1000); // 表示時間 (ミリ秒)
631
+ } else {
632
+ console.warn("Correct effect element not found.");
633
  }
634
  }
635
 
636
+ /**
637
+ * 正解エフェクトを非表示にする
638
+ */
639
  function hideCorrectEffect() {
640
  if (correctEffect && correctEffect.classList.contains('show')) {
641
+ correctEffect.classList.remove('show'); // 表示クラスを削除
 
 
 
 
 
642
  }
643
+ // タイマーもクリアしておく(手動で非表示にする場合など)
644
+ clearTimeout(correctEffectTimeout);
645
  }
646
 
647
 
648
  // --- settings.html 用の処理 ---
649
 
650
  /**
651
+ * トグルスイッチの状態が変更されたときの処理
652
+ * @param {HTMLInputElement} checkbox - 変更されたチェックボックス要素
653
+ * @param {string} type - トグルの種類 ('dark', 'notification' など)
654
  */
655
  function handleToggleChange(checkbox, type) {
656
+ const isChecked = checkbox.checked;
657
+ console.log(`Toggle changed for ${type}: ${isChecked}`);
658
+
659
  if (type === 'dark') {
660
+ // ダークモードの切り替え
661
+ document.body.classList.toggle('dark-mode', isChecked);
662
+ // 設定をlocalStorageに保存 (エラーハンドリング付き)
663
  try {
664
+ localStorage.setItem('darkModeEnabled', isChecked);
665
+ console.log(`Dark mode preference saved: ${isChecked}`);
666
  } catch (e) {
667
+ console.warn('Could not save dark mode preference to localStorage:', e);
668
+ // ユーザーに通知する (任意)
669
+ // alert('ダークモード設定の保存に失敗しました。');
670
  }
671
+ } else if (type === 'notification') {
672
+ // 通知設定の切り替え (未実装)
673
+ console.log("Notification setting toggled:", isChecked);
674
+ alert(`通知設定は現在未実装です。(設定: ${isChecked ? 'ON' : 'OFF'})`);
675
+ // ここに通知設定のAPI呼び出しなどを実装
676
  }
677
+ // 他のトグルスイッチの処理もここに追加
678
  }
679
 
680
  /**
681
+ * ログアウトボタンがクリックされたときの処理
682
  */
683
  function handleLogout() {
684
+ console.log("Logout button clicked");
685
+ // TODO: 実際のログアウト処理を実装
686
+ // - サーバーAPIへのログアウト要求
687
+ // - ローカルストレージやセッションストレージの認証情報クリア
688
+ // - など
689
+
690
+ // 仮の動作: アラート表示と入力画面への遷移
691
+ alert("ログアウトしました。(この機能は現在開発中です)");
692
+ // navigateTo 内でメニューが閉じる
693
+ goToInput();
694
  }
695
 
696
  /**
697
+ * ローカルストレージからダークモード設定を読み込み、bodyに適用する
698
  */
699
  function applyDarkModePreference() {
700
  try {
701
+ // localStorageから 'darkModeEnabled' の値を取得し、'true' かどうかで判定
702
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
703
  document.body.classList.toggle('dark-mode', darkModeEnabled);
704
+ console.log(`Applied dark mode preference from localStorage: ${darkModeEnabled}`);
 
 
705
  } catch (e) {
706
+ console.warn('Could not load or apply dark mode preference from localStorage:', e);
707
+ // ここでエラーが発生しても処理は続行する
708
  }
709
  }
710
 
711
 
712
  // --- ページの初期化処理 ---
713
+ const pathname = window.location.pathname; // 現在のパスをグローバルスコープで取得
714
 
715
+ // ★★★ 最初にダークモード設定を適用 (FOUC: Flash of Unstyled Content 対策) ★★★
716
  applyDarkModePreference();
717
 
718
+ // ★★★ DOMContentLoaded イベントリスナー: DOM構築完了後に実行 ★★★
719
  document.addEventListener('DOMContentLoaded', () => {
720
+ console.log('DOM fully loaded and parsed. Path:', pathname);
721
+
722
+ // メニュー要素を取得してグローバル変数に格納
723
+ sideMenu = document.getElementById('side-menu');
724
+ menuOverlay = document.getElementById('menu-overlay');
725
+ if (!sideMenu || !menuOverlay) {
726
+ console.warn("Side menu or overlay element not found on this page.");
727
+ }
728
 
729
+ // learning.html の場合のみ正解エフェクト要素を取得し、初期化関数を呼ぶ
730
+ if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) {
731
+ correctEffect = document.getElementById('correct-effect');
732
+ if (!correctEffect) {
733
+ console.warn("Correct effect element ('correct-effect') not found on learning page.");
734
+ }
735
+ initializeLearningScreen(); // learningページ専用の初期化
736
+ }
737
+
738
+ // input.html の初期化処理
739
+ if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) {
740
+ console.log("Initializing Input page...");
741
  const form = document.getElementById('generate-form');
742
  if (form) {
743
+ // フォーム送信イベントにリスナーを設定
744
  form.addEventListener('submit', (event) => {
745
+ event.preventDefault(); // デフォルトの送信をキャンセル
746
+ handleGenerateSubmit(); // カスタム送信処理を呼び出し
747
  });
748
+ } else {
749
+ console.warn("Generate form ('generate-form') not found.");
750
  }
751
+ // URLパラメータから初期値を設定する例 (任意)
 
752
  const urlParams = new URLSearchParams(window.location.search);
753
  const initialUrl = urlParams.get('url');
754
  if (initialUrl) {
755
  const urlInput = document.getElementById('youtube-url');
756
  if (urlInput) {
757
  urlInput.value = initialUrl;
758
+ console.log("Set initial URL from query parameter:", initialUrl);
759
  }
760
  }
761
+ }
762
+ // history.html の初期化処理 (必要なら)
763
+ else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) {
764
+ console.log("Initializing History page...");
765
+ // 例: loadHistoryData(); // 履歴データを読み込んで表示する関数
766
+ }
767
+ // settings.html の初期化処理
768
+ else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) {
769
+ console.log("Initializing Settings page...");
 
770
  // ダークモードトグルの状態をlocalStorageに合わせて更新
771
  try {
772
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
773
+ // type='dark' を持つトグルスイッチを探す
774
+ const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]');
775
  if (toggle) {
776
  toggle.checked = darkModeEnabled;
777
+ console.log("Set dark mode toggle state to:", darkModeEnabled);
778
+ } else {
779
+ console.warn("Dark mode toggle switch not found on settings page.");
780
  }
781
  } catch (e) {
782
+ console.warn('Could not set dark mode toggle state on settings page:', e);
783
  }
784
  }
785
 
786
+ // フッターナビゲーションのアクティブ状態を更新
787
  updateFooterNavActiveState(pathname);
788
+
789
+ // ★★★ ページ読み込み時にメニューが意図せず開いた状態なら閉じる ★★★
790
+ // (特にブラウザの履歴操作などで発生することがあるため)
791
+ closeMenu();
792
  });
793
 
794
 
795
  /**
796
+ * 現在のパスに基づいてフッターナビゲーションのアクティブ状態を更新
797
  */
798
  function updateFooterNavActiveState(currentPath) {
799
  const footerNav = document.querySelector('.footer-nav');
800
+ if (!footerNav) {
801
+ // フッターがないページなら何もしない
802
+ return;
803
+ }
804
 
805
  const buttons = footerNav.querySelectorAll('button');
806
+ let foundActive = false; // アクティブなボタンが見つかったか
807
+
808
  buttons.forEach(button => {
809
+ button.classList.remove('active'); // まず全て非アクティブに
810
  const onclickAttr = button.getAttribute('onclick');
811
  if (onclickAttr) {
812
+ // onclick属性の値から遷移先を判定
813
  if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) {
814
  button.classList.add('active');
815
+ foundActive = true;
816
  } else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) {
817
  button.classList.add('active');
818
+ foundActive = true;
819
  } else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) {
820
  button.classList.add('active');
821
+ foundActive = true;
822
  }
823
+ // learning ページは通常フッターをアクティブにしない想定
 
 
 
824
  }
825
  });
826
+
827
+ // デバッグ用ログ
828
+ // console.log(`Footer nav active state updated for path "${currentPath}". Active button found: ${foundActive}`);
829
  }
830
 
831
 
832
+ // --- デバッグ用: 一部関数/変数をグローバルスコープに公開 ---
833
+ // 注意: 本番環境では削除またはコメントアウトすることを強く推奨します
834
  window.debug = {
835
  navigateTo,
836
  goToInput,
 
838
  goToSettings,
839
  goToLearning,
840
  openMenu,
841
+ closeMenu,
842
  handleGenerateSubmit,
843
  initializeLearningScreen,
844
+ handleOptionClick,
845
  revealAnswer,
846
  goToNext,
847
  goToPrev,
848
  handleToggleChange,
849
  handleLogout,
850
+ showCorrectEffect,
851
+ hideCorrectEffect,
852
  learningData, // 現在の学習データ確認用
853
+ currentItemIndex, // 現在のインデックス確認用
854
+ sideMenu, // メニュー要素確認用
855
+ menuOverlay // オーバーレイ要素確認用
856
  };
857
 
858
  // --- END OF FILE script.js ---
static/style.css CHANGED
@@ -597,4 +597,162 @@ body.dark-mode .toggle-switch input:checked + .slider { background-color: #58a6f
597
  /* ダークモードの正解エフェクトの色 */
598
  body.dark-mode .correct-effect { color: rgba(50, 220, 50, 0.85); }
599
 
600
- /* --- END OF FILE style.css --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  /* ダークモードの正解エフェクトの色 */
598
  body.dark-mode .correct-effect { color: rgba(50, 220, 50, 0.85); }
599
 
600
+ /* --- END OF FILE style.css --- */
601
+ /* --- START OF FILE style.css (追記部分) --- */
602
+
603
+ /* --- サイドメニュー --- */
604
+ .side-menu {
605
+ position: fixed;
606
+ top: 0;
607
+ left: 0;
608
+ width: 250px; /* メニューの幅 */
609
+ height: 100%;
610
+ background-color: #ffffff; /* メニューの背景色 */
611
+ box-shadow: 2px 0 5px rgba(0,0,0,0.1);
612
+ transform: translateX(-100%); /* 初期状態は画面外(左側) */
613
+ transition: transform 0.3s ease-in-out;
614
+ z-index: 1100; /* オーバーレイより手前 */
615
+ padding: 20px;
616
+ box-sizing: border-box;
617
+ display: flex;
618
+ flex-direction: column;
619
+ overflow-y: auto; /* メニュー項目が多い場合にスクロール */
620
+ }
621
+
622
+ .side-menu.open {
623
+ transform: translateX(0); /* 表示状態 */
624
+ }
625
+
626
+ .side-menu h2 {
627
+ margin-top: 30px; /* 閉じるボタンとのスペース */
628
+ margin-bottom: 20px;
629
+ font-size: 20px;
630
+ color: #333;
631
+ border-bottom: 1px solid #eee;
632
+ padding-bottom: 10px;
633
+ }
634
+
635
+ .side-menu ul {
636
+ list-style: none;
637
+ padding: 0;
638
+ margin: 0;
639
+ flex-grow: 1;
640
+ }
641
+
642
+ .side-menu li {
643
+ margin-bottom: 5px;
644
+ }
645
+
646
+ .side-menu li button {
647
+ background: none;
648
+ border: none;
649
+ padding: 12px 10px;
650
+ width: 100%;
651
+ text-align: left;
652
+ font-size: 16px;
653
+ color: #333;
654
+ cursor: pointer;
655
+ border-radius: 6px;
656
+ transition: background-color 0.2s;
657
+ display: flex; /* アイコンとテキストのため */
658
+ align-items: center;
659
+ }
660
+ .side-menu li button:hover {
661
+ background-color: #f0f0f0;
662
+ }
663
+ .side-menu li button:active {
664
+ background-color: #e0e0e0;
665
+ }
666
+
667
+ .side-menu li hr {
668
+ border: none;
669
+ border-top: 1px solid #eee;
670
+ margin: 15px 0;
671
+ }
672
+
673
+ .side-menu .logout-menu-item {
674
+ color: red; /* ログアウトは赤字 */
675
+ }
676
+ .side-menu .logout-menu-item:hover {
677
+ background-color: rgba(255, 0, 0, 0.05);
678
+ }
679
+
680
+
681
+ .close-menu-btn {
682
+ position: absolute;
683
+ top: 10px;
684
+ right: 15px;
685
+ background: none;
686
+ border: none;
687
+ font-size: 30px;
688
+ font-weight: bold;
689
+ color: #888;
690
+ cursor: pointer;
691
+ padding: 5px;
692
+ line-height: 1;
693
+ }
694
+ .close-menu-btn:hover {
695
+ color: #333;
696
+ }
697
+
698
+ /* --- メニューオーバーレイ --- */
699
+ .menu-overlay {
700
+ position: fixed;
701
+ top: 0;
702
+ left: 0;
703
+ width: 100%;
704
+ height: 100%;
705
+ background-color: rgba(0, 0, 0, 0.5); /* 半透明の黒 */
706
+ opacity: 0;
707
+ visibility: hidden; /* 初期状態は非表示 */
708
+ transition: opacity 0.3s ease-in-out, visibility 0s 0.3s; /* visibilityは遅延させる */
709
+ z-index: 1050; /* サイドメニューより下、他コンテンツより上 */
710
+ }
711
+
712
+ .menu-overlay.open {
713
+ opacity: 1;
714
+ visibility: visible;
715
+ transition: opacity 0.3s ease-in-out, visibility 0s 0s; /* 表示時は遅延なし */
716
+ }
717
+
718
+ /* --- メニュー表示中に背景をスクロールさせない (任意) --- */
719
+ body.menu-open {
720
+ overflow: hidden; /* bodyのスクロールを禁止 */
721
+ }
722
+
723
+ /* --- ダークモード用サイドメニュー (追記) --- */
724
+ body.dark-mode .side-menu {
725
+ background-color: #2c2c2c; /* ダークモードの背景色 */
726
+ box-shadow: 2px 0 5px rgba(0,0,0,0.3);
727
+ }
728
+ body.dark-mode .side-menu h2 {
729
+ color: #e0e0e0;
730
+ border-bottom-color: #444;
731
+ }
732
+ body.dark-mode .side-menu li button {
733
+ color: #e0e0e0;
734
+ }
735
+ body.dark-mode .side-menu li button:hover {
736
+ background-color: #3a3a3a;
737
+ }
738
+ body.dark-mode .side-menu li button:active {
739
+ background-color: #4a4a4a;
740
+ }
741
+ body.dark-mode .side-menu li hr {
742
+ border-top-color: #444;
743
+ }
744
+ body.dark-mode .close-menu-btn {
745
+ color: #aaa;
746
+ }
747
+ body.dark-mode .close-menu-btn:hover {
748
+ color: #e0e0e0;
749
+ }
750
+ body.dark-mode .side-menu .logout-menu-item {
751
+ color: #ff7f7f; /* ダークモードでの赤 */
752
+ }
753
+ body.dark-mode .side-menu .logout-menu-item:hover {
754
+ background-color: rgba(255, 80, 80, 0.1);
755
+ }
756
+
757
+
758
+ /* --- END OF FILE style.css (追記部分) --- */
templates/history.html CHANGED
@@ -86,6 +86,20 @@
86
  <li class="list-item-empty">履歴はありません。</li>
87
  {% endif %}
88
  </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </main>
90
  <!-- フッターナビゲーション -->
91
  <footer class="footer-nav">
 
86
  <li class="list-item-empty">履歴はありません。</li>
87
  {% endif %}
88
  </ul>
89
+ <!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
90
+ <div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
91
+ <nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
92
+ <button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
93
+ <h2>メニュー</h2>
94
+ <ul>
95
+ <li><button onclick="goToInput()">➕ 入力</button></li>
96
+ <li><button onclick="goToHistory()">🕒 履歴</button></li>
97
+ <li><button onclick="goToSettings()">⚙️ 設定</button></li>
98
+ <!-- 他に必要なメニュー項目を追加 -->
99
+ <li><hr></li>
100
+ <li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
101
+ </ul>
102
+ </nav>
103
  </main>
104
  <!-- フッターナビゲーション -->
105
  <footer class="footer-nav">
templates/input.html CHANGED
@@ -41,6 +41,20 @@
41
  <span>(イメージ表示エリア)</span>
42
  <!-- <img src="..." alt="動画サムネイル" style="display: none;"> -->
43
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </main>
45
  <!-- フッターナビゲーションなどを追加する場合はここに -->
46
  </div>
 
41
  <span>(イメージ表示エリア)</span>
42
  <!-- <img src="..." alt="動画サムネイル" style="display: none;"> -->
43
  </div>
44
+ <!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
45
+ <div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
46
+ <nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
47
+ <button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
48
+ <h2>メニュー</h2>
49
+ <ul>
50
+ <li><button onclick="goToInput()">➕ 入力</button></li>
51
+ <li><button onclick="goToHistory()">🕒 履歴</button></li>
52
+ <li><button onclick="goToSettings()">⚙️ 設定</button></li>
53
+ <!-- 他に必要なメニュー項目を追加 -->
54
+ <li><hr></li>
55
+ <li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
56
+ </ul>
57
+ </nav>
58
  </main>
59
  <!-- フッターナビゲーションなどを追加する場合はここに -->
60
  </div>
templates/learning.html CHANGED
@@ -38,6 +38,20 @@
38
  <span id="page-info">? / ?</span> <!-- 初期表示 -->
39
  <button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
40
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </main>
42
  </div>
43
 
 
38
  <span id="page-info">? / ?</span> <!-- 初期表示 -->
39
  <button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
40
  </div>
41
+ <!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
42
+ <div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
43
+ <nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
44
+ <button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
45
+ <h2>メニュー</h2>
46
+ <ul>
47
+ <li><button onclick="goToInput()">➕ 入力</button></li>
48
+ <li><button onclick="goToHistory()">🕒 履歴</button></li>
49
+ <li><button onclick="goToSettings()">⚙️ 設定</button></li>
50
+ <!-- 他に必要なメニュー項目を追加 -->
51
+ <li><hr></li>
52
+ <li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
53
+ </ul>
54
+ </nav>
55
  </main>
56
  </div>
57
 
templates/settings.html CHANGED
@@ -77,6 +77,20 @@
77
  </button>
78
  </li>
79
  </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </main>
81
  </div>
82
  <script src="{{ url_for('static', filename='script.js') }}"></script>
 
77
  </button>
78
  </li>
79
  </ul>
80
+ <!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
81
+ <div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
82
+ <nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
83
+ <button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
84
+ <h2>メニュー</h2>
85
+ <ul>
86
+ <li><button onclick="goToInput()">➕ 入力</button></li>
87
+ <li><button onclick="goToHistory()">🕒 履歴</button></li>
88
+ <li><button onclick="goToSettings()">⚙️ 設定</button></li>
89
+ <!-- 他に必要なメニュー項目を追加 -->
90
+ <li><hr></li>
91
+ <li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
92
+ </ul>
93
+ </nav>
94
  </main>
95
  </div>
96
  <script src="{{ url_for('static', filename='script.js') }}"></script>