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