diff --git a/css/main.css b/css/main.css index 164ad9c..3d1b2a1 100644 --- a/css/main.css +++ b/css/main.css @@ -1074,4 +1074,116 @@ th { .thinking-bubbles { transform: translateX(-50%) scale(0.8); } +} + +/* ===== 問題タイプ選択 ===== */ + +.challenge-meta { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1rem; +} + +.challenge-type-selector { + background: #f8f9fa; + border-radius: 12px; + padding: 1rem; + border: 1px solid #e2e8f0; +} + +.selector-label { + font-weight: 600; + color: #4a5568; + font-size: 0.9rem; + margin-bottom: 0.5rem; + display: block; +} + +.type-toggle { + display: flex; + background: #e2e8f0; + border-radius: 8px; + padding: 4px; + margin-bottom: 0.75rem; +} + +.type-button { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border: none; + border-radius: 6px; + background: transparent; + color: #718096; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; +} + +.type-button:hover { + background: rgba(255, 255, 255, 0.5); + color: #4a5568; +} + +.type-button.active { + background: white; + color: #667eea; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-weight: 600; +} + +.type-icon { + font-size: 1.1rem; +} + +.type-text { + font-size: 0.85rem; +} + +.type-description { + text-align: center; + color: #718096; + font-size: 0.8rem; + font-style: italic; +} + +/* レスポンシブ対応 */ +@media (max-width: 768px) { + .challenge-meta { + gap: 0.75rem; + } + + .type-button { + padding: 0.6rem 0.75rem; + gap: 0.4rem; + } + + .type-text { + font-size: 0.8rem; + } + + .type-icon { + font-size: 1rem; + } + + .challenge-type-selector { + padding: 0.75rem; + } +} + +@media (max-width: 480px) { + .type-toggle { + flex-direction: column; + gap: 4px; + } + + .type-button { + justify-content: flex-start; + padding: 0.75rem; + } } \ No newline at end of file diff --git a/css/word-reorder.css b/css/word-reorder.css index 4eb1717..6c3b3d0 100644 --- a/css/word-reorder.css +++ b/css/word-reorder.css @@ -104,7 +104,7 @@ transition: all 0.2s ease; border: 2px solid transparent; position: relative; - + /* タップターゲットサイズを確保(最小44px) */ min-height: 44px; min-width: 44px; @@ -184,9 +184,11 @@ transform: scale(0.8); opacity: 0; } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; @@ -266,9 +268,11 @@ 0% { transform: scale(1); } + 50% { transform: scale(1.02); } + 100% { transform: scale(1); } @@ -281,28 +285,28 @@ .word-reorder-container { padding: 0.8rem; } - + .instruction { padding: 0.8rem; margin-bottom: 1.2rem; } - + .instruction p { font-size: 1rem; } - + .word-token { padding: 0.5rem 0.8rem; font-size: 0.9rem; min-height: 40px; min-width: 40px; } - + .controls { flex-direction: column; align-items: center; } - + .control-button { width: 100%; max-width: 300px; @@ -314,34 +318,35 @@ .word-reorder-container { padding: 0.5rem; } - + .instruction { padding: 0.6rem; margin-bottom: 1rem; } - + .instruction p { font-size: 0.95rem; } - + .answer-area h4, .available-area h4 { font-size: 1.1rem; } - + .selected-words, .available-words { padding: 0.8rem; gap: 0.4rem; } - + .word-token { padding: 0.4rem 0.7rem; font-size: 0.85rem; - min-height: 44px; /* タップターゲットサイズを維持 */ + min-height: 44px; + /* タップターゲットサイズを維持 */ min-width: 44px; } - + .control-button { padding: 0.7rem 1.2rem; font-size: 0.95rem; @@ -357,7 +362,7 @@ min-height: 44px; min-width: 44px; } - + .selected-words, .available-words { padding: 0.6rem; @@ -370,27 +375,27 @@ .word-reorder-container { color: #e2e8f0; } - + .answer-area h4, .available-area h4 { color: #e2e8f0; } - + .selected-words { background-color: #2d3748; border-color: #4a5568; } - + .selected-words:not(:empty) { border-color: #48bb78; background-color: #1a202c; } - + .available-words { background-color: #2d3748; border-color: #4a5568; } - + .empty-message { color: #718096; } @@ -398,17 +403,18 @@ /* アクセシビリティ */ @media (prefers-reduced-motion: reduce) { + .word-token, .control-button, .selected-words, .word-reorder-container { transition: none; } - + .word-token.word-added { animation: none; } - + .word-reorder-container.reset-animation { animation: none; } @@ -426,7 +432,7 @@ .word-token { border-width: 3px; } - + .selected-words, .available-words { border-width: 3px; @@ -442,7 +448,8 @@ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); z-index: 1000; pointer-events: none; - transition: none; /* ドラッグ中はトランジションを無効化 */ + transition: none; + /* ドラッグ中はトランジションを無効化 */ } /* ドロップゾーンのハイライト */ @@ -502,10 +509,13 @@ /* アニメーション定義 */ @keyframes insertionPulse { - 0%, 100% { + + 0%, + 100% { opacity: 0.6; transform: scaleY(0.8); } + 50% { opacity: 1; transform: scaleY(1.2); @@ -513,9 +523,12 @@ } @keyframes highlightPulse { - 0%, 100% { + + 0%, + 100% { background-color: rgba(66, 153, 225, 0.05); } + 50% { background-color: rgba(66, 153, 225, 0.15); } @@ -526,10 +539,12 @@ transform: scale(1.2); background-color: rgba(72, 187, 120, 0.3); } + 50% { transform: scale(1.1); background-color: rgba(72, 187, 120, 0.2); } + 100% { transform: scale(1); background-color: transparent; @@ -537,13 +552,17 @@ } @keyframes dropFailed { - 0%, 100% { + + 0%, + 100% { transform: translateX(0); } + 25% { transform: translateX(-10px); background-color: rgba(245, 101, 101, 0.2); } + 75% { transform: translateX(10px); background-color: rgba(245, 101, 101, 0.2); @@ -574,12 +593,12 @@ height: 35px; width: 4px; } - + .drag-preview { font-size: 0.85rem; padding: 0.3rem 0.6rem; } - + .word-token.dragging { transform: rotate(3deg) scale(1.03); } @@ -590,12 +609,12 @@ height: 30px; width: 4px; } - + .drag-preview { font-size: 0.8rem; padding: 0.25rem 0.5rem; } - + .word-token.dragging { transform: rotate(2deg) scale(1.02); } @@ -606,11 +625,11 @@ .word-token { cursor: default; } - + .word-token:active { cursor: default; } - + /* タッチデバイスでのドラッグフィードバック強化 */ .word-token.dragging { opacity: 0.8; @@ -625,20 +644,20 @@ transform: none; animation: none; } - + .insertion-indicator { animation: none; } - + .insertion-highlight { animation: none; } - + .word-token.drop-success, .word-token.drop-failed { animation: none; } - + .drag-over { transform: none; } @@ -650,12 +669,12 @@ border-width: 4px; border-color: #000; } - + .drag-over { border-width: 4px; border-color: #0066cc; } - + .insertion-indicator { background: #0066cc; width: 5px; @@ -674,6 +693,7 @@ white-space: nowrap; border: 0; } + /* ゲームオーバーレイ(アニメーション用) */ .game-overlay { @@ -707,10 +727,13 @@ } @keyframes pulse { - 0%, 100% { + + 0%, + 100% { opacity: 0.7; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.05); @@ -748,10 +771,12 @@ opacity: 0; transform: scale(0.3); } + 50% { opacity: 1; transform: scale(1.1); } + 100% { opacity: 1; transform: scale(1); @@ -793,6 +818,7 @@ transform: translateY(-100vh) rotate(0deg); opacity: 1; } + 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; diff --git a/index.html b/index.html index 201f2bd..255e786 100644 --- a/index.html +++ b/index.html @@ -35,9 +35,29 @@

📊 データベース構造

データベースを初期化中...

しばらくお待ちください。

-
- 難易度: -
+
+
+ 難易度: +
+
+ + +
diff --git a/js/ui-controller.js b/js/ui-controller.js index 3b0103c..39d9140 100644 --- a/js/ui-controller.js +++ b/js/ui-controller.js @@ -5,6 +5,8 @@ export class UIController { this.currentHintLevel = 0; this.wordReorderUI = null; this.sqlTokenizer = null; + this.currentChallengeType = null; // 現在選択されている問題タイプ + this.isMobile = this.detectMobileDevice(); // モバイルデバイス検出 this.initializeElements(); this.bindEvents(); // ゲームオーバーレイを初期状態で非表示 @@ -81,9 +83,136 @@ export class UIController { }); this.elements.toggleSidebar.addEventListener('click', () => this.toggleSidebar()); + + // 問題タイプ選択のイベントリスナーを追加 + this.bindChallengeTypeEvents(); } - updateChallenge() { + /** + * モバイルデバイスを検出 + * @returns {boolean} モバイルデバイスかどうか + */ + detectMobileDevice() { + const userAgent = navigator.userAgent.toLowerCase(); + const mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone']; + + // ユーザーエージェントでの検出 + const isMobileUA = mobileKeywords.some(keyword => userAgent.includes(keyword)); + + // 画面サイズでの検出 + const isMobileScreen = window.innerWidth <= 768; + + // タッチデバイスの検出 + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + + return isMobileUA || (isMobileScreen && isTouchDevice); + } + + /** + * 問題タイプ選択のイベントリスナーを設定 + */ + bindChallengeTypeEvents() { + const sqlEditorButton = document.getElementById('type-sql-editor'); + const wordReorderButton = document.getElementById('type-word-reorder'); + const typeDescriptionText = document.getElementById('type-description-text'); + + if (sqlEditorButton && wordReorderButton) { + sqlEditorButton.addEventListener('click', () => { + this.switchChallengeType('challenge'); + this.updateTypeButtons('challenge'); + if (typeDescriptionText) { + typeDescriptionText.textContent = 'SQLを直接入力して実行します'; + } + }); + + wordReorderButton.addEventListener('click', () => { + this.switchChallengeType('word-reorder'); + this.updateTypeButtons('word-reorder'); + if (typeDescriptionText) { + typeDescriptionText.textContent = '単語をタップして正しい順序に並び替えます'; + } + }); + } + } + + /** + * 問題タイプボタンの表示を更新 + * @param {string} activeType - アクティブなタイプ + */ + updateTypeButtons(activeType) { + const sqlEditorButton = document.getElementById('type-sql-editor'); + const wordReorderButton = document.getElementById('type-word-reorder'); + + if (sqlEditorButton && wordReorderButton) { + sqlEditorButton.classList.toggle('active', activeType === 'challenge'); + wordReorderButton.classList.toggle('active', activeType === 'word-reorder'); + } + } + + /** + * 問題タイプを切り替え + * @param {string} type - 切り替える問題タイプ + */ + switchChallengeType(type) { + this.currentChallengeType = type; + const challenge = this.gameEngine.getCurrentChallenge(); + + // スライドタイプの場合は切り替えできない + if (challenge.type === 'slide') { + return; + } + + // 問題タイプに応じて表示を切り替え + if (type === 'word-reorder') { + this.showWordReorderChallenge(challenge); + } else { + this.showSQLChallenge(); + } + } + + /** + * 問題タイプ選択UIの表示/非表示を制御 + * @param {Object} challenge - 現在のチャレンジ + */ + updateChallengeTypeSelector(challenge) { + const typeSelector = document.getElementById('challenge-type-selector'); + + if (!typeSelector) return; + + // スライドタイプの場合は選択UIを非表示 + if (challenge.type === 'slide') { + typeSelector.classList.add('hidden'); + return; + } + + // SQL実行可能な問題の場合は選択UIを表示 + if (challenge.solution) { + typeSelector.classList.remove('hidden'); + + // デフォルトの問題タイプを設定 + if (!this.currentChallengeType) { + this.currentChallengeType = this.isMobile ? 'word-reorder' : 'challenge'; + } + + // ボタンの状態を更新 + this.updateTypeButtons(this.currentChallengeType); + + // 説明文を更新 + const typeDescriptionText = document.getElementById('type-description-text'); + if (typeDescriptionText) { + typeDescriptionText.textContent = this.currentChallengeType === 'word-reorder' + ? '単語をタップして正しい順序に並び替えます' + : 'SQLを直接入力して実行します'; + } + } else { + typeSelector.classList.add('hidden'); + } + } + + /** + * updateChallengeメソッドを拡張して問題タイプ選択を含める + */ + updateChallengeWithTypeSelection() { const challenge = this.gameEngine.getCurrentChallenge(); const progress = this.gameEngine.getProgress(); @@ -104,12 +233,15 @@ export class UIController { this.elements.prevButton.disabled = progress.current === 1; this.elements.nextButton.disabled = progress.current === progress.total; + // 問題タイプ選択UIを更新 + this.updateChallengeTypeSelector(challenge); + // チャレンジタイプによって表示を切り替え console.log('Current challenge:', challenge.title, 'Type:', challenge.type); if (challenge.type === 'slide') { console.log('Showing slide challenge'); this.showSlideChallenge(challenge); - } else if (challenge.type === 'word-reorder') { + } else if (challenge.type === 'word-reorder' || this.currentChallengeType === 'word-reorder') { console.log('Showing word-reorder challenge'); this.showWordReorderChallenge(challenge); } else { @@ -126,6 +258,11 @@ export class UIController { this.clearResults(); } + updateChallenge() { + // 新しい問題タイプ選択機能付きのメソッドを呼び出し + this.updateChallengeWithTypeSelection(); + } + async executeQuery() { const sql = this.elements.sqlEditor.value.trim(); if (!sql) { @@ -1013,4 +1150,174 @@ export class UIController { this.endGameAnimation(); } } + + /** + * モバイルデバイスを検出 + * @returns {boolean} モバイルデバイスかどうか + */ + detectMobileDevice() { + const userAgent = navigator.userAgent.toLowerCase(); + const mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone']; + + // ユーザーエージェントでの検出 + const isMobileUA = mobileKeywords.some(keyword => userAgent.includes(keyword)); + + // 画面サイズでの検出 + const isMobileScreen = window.innerWidth <= 768; + + // タッチデバイスの検出 + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + + return isMobileUA || (isMobileScreen && isTouchDevice); + } + + /** + * 問題タイプ選択のイベントリスナーを設定 + */ + bindChallengeTypeEvents() { + const sqlEditorButton = document.getElementById('type-sql-editor'); + const wordReorderButton = document.getElementById('type-word-reorder'); + const typeDescriptionText = document.getElementById('type-description-text'); + + if (sqlEditorButton && wordReorderButton) { + sqlEditorButton.addEventListener('click', () => { + this.switchChallengeType('challenge'); + this.updateTypeButtons('challenge'); + if (typeDescriptionText) { + typeDescriptionText.textContent = 'SQLを直接入力して実行します'; + } + }); + + wordReorderButton.addEventListener('click', () => { + this.switchChallengeType('word-reorder'); + this.updateTypeButtons('word-reorder'); + if (typeDescriptionText) { + typeDescriptionText.textContent = '単語をタップして正しい順序に並び替えます'; + } + }); + } + } + + /** + * 問題タイプボタンの表示を更新 + * @param {string} activeType - アクティブなタイプ + */ + updateTypeButtons(activeType) { + const sqlEditorButton = document.getElementById('type-sql-editor'); + const wordReorderButton = document.getElementById('type-word-reorder'); + + if (sqlEditorButton && wordReorderButton) { + sqlEditorButton.classList.toggle('active', activeType === 'challenge'); + wordReorderButton.classList.toggle('active', activeType === 'word-reorder'); + } + } + + /** + * 問題タイプを切り替え + * @param {string} type - 切り替える問題タイプ + */ + switchChallengeType(type) { + this.currentChallengeType = type; + const challenge = this.gameEngine.getCurrentChallenge(); + + // スライドタイプの場合は切り替えできない + if (challenge.type === 'slide') { + return; + } + + // 問題タイプに応じて表示を切り替え + if (type === 'word-reorder') { + this.showWordReorderChallenge(challenge); + } else { + this.showSQLChallenge(); + } + } + + /** + * 問題タイプ選択UIの表示/非表示を制御 + * @param {Object} challenge - 現在のチャレンジ + */ + updateChallengeTypeSelector(challenge) { + const typeSelector = document.getElementById('challenge-type-selector'); + + if (!typeSelector) return; + + // スライドタイプの場合は選択UIを非表示 + if (challenge.type === 'slide') { + typeSelector.classList.add('hidden'); + return; + } + + // SQL実行可能な問題の場合は選択UIを表示 + if (challenge.solution) { + typeSelector.classList.remove('hidden'); + + // デフォルトの問題タイプを設定 + if (!this.currentChallengeType) { + this.currentChallengeType = this.isMobile ? 'word-reorder' : 'challenge'; + } + + // ボタンの状態を更新 + this.updateTypeButtons(this.currentChallengeType); + + // 説明文を更新 + const typeDescriptionText = document.getElementById('type-description-text'); + if (typeDescriptionText) { + typeDescriptionText.textContent = this.currentChallengeType === 'word-reorder' + ? '単語をタップして正しい順序に並び替えます' + : 'SQLを直接入力して実行します'; + } + } else { + typeSelector.classList.add('hidden'); + } + } + + /** + * updateChallengeメソッドを拡張して問題タイプ選択を含める + */ + updateChallengeWithTypeSelection() { + const challenge = this.gameEngine.getCurrentChallenge(); + const progress = this.gameEngine.getProgress(); + + this.elements.challengeTitle.textContent = challenge.title || 'タイトル未設定'; + this.elements.challengeDescription.textContent = challenge.description || ''; + this.elements.currentStage.textContent = progress.current; + this.elements.progressFill.style.width = `${progress.percentage}%`; + + // 難易度表示(デフォルト値を設定) + const difficulty = Math.min(Math.max(challenge.difficulty || 1, 1), 10); + const filledStars = Math.min(difficulty, 5); + const emptyStars = Math.max(5 - filledStars, 0); + this.elements.difficultyStars.innerHTML = '★'.repeat(filledStars) + + '☆'.repeat(emptyStars) + + (difficulty > 5 ? ` (${difficulty}/10)` : ''); + + // ボタン状態更新 + this.elements.prevButton.disabled = progress.current === 1; + this.elements.nextButton.disabled = progress.current === progress.total; + + // 問題タイプ選択UIを更新 + this.updateChallengeTypeSelector(challenge); + + // チャレンジタイプによって表示を切り替え + console.log('Current challenge:', challenge.title, 'Type:', challenge.type); + if (challenge.type === 'slide') { + console.log('Showing slide challenge'); + this.showSlideChallenge(challenge); + } else if (challenge.type === 'word-reorder' || this.currentChallengeType === 'word-reorder') { + console.log('Showing word-reorder challenge'); + this.showWordReorderChallenge(challenge); + } else { + console.log('Showing SQL challenge'); + this.showSQLChallenge(); + } + + // ヒントレベルリセット + this.currentHintLevel = 0; + this.hideHint(); + + // エディタクリア + this.elements.sqlEditor.value = ''; + this.clearResults(); + } } \ No newline at end of file