Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Vocabulary Master - 100 Common English Words</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .card { | |
| perspective: 1000px; | |
| } | |
| .card-inner { | |
| transition: transform 0.8s; | |
| transform-style: preserve-3d; | |
| } | |
| .card.flipped .card-inner { | |
| transform: rotateY(180deg); | |
| } | |
| .card-front, .card-back { | |
| backface-visibility: hidden; | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .card-back { | |
| transform: rotateY(180deg); | |
| } | |
| .progress-bar { | |
| transition: width 0.5s ease-in-out; | |
| } | |
| .shake { | |
| animation: shake 0.5s; | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 20%, 60% { transform: translateX(-5px); } | |
| 40%, 80% { transform: translateX(5px); } | |
| } | |
| .bounce { | |
| animation: bounce 0.5s; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .word-category { | |
| background-color: rgba(99, 102, 241, 0.1); | |
| border-left: 4px solid #6366f1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold text-indigo-800 mb-2">Vocabulary Master</h1> | |
| <p class="text-gray-600">Learn 100 common English words effectively</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Vocabulary Input Section --> | |
| <div class="bg-white rounded-xl shadow-lg p-6"> | |
| <h2 class="text-2xl font-semibold text-indigo-700 mb-4">Add New Words</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-gray-700 mb-2">English Word</label> | |
| <input type="text" id="englishWord" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Vietnamese Meaning</label> | |
| <input type="text" id="vietnameseMeaning" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Example Sentence (Optional)</label> | |
| <input type="text" id="exampleSentence" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <button id="addWordBtn" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-lg hover:bg-indigo-700 transition flex items-center justify-center"> | |
| <i class="fas fa-plus-circle mr-2"></i> Add Word | |
| </button> | |
| </div> | |
| <div class="mt-6"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-3">Your Vocabulary List</h3> | |
| <div id="wordList" class="max-h-60 overflow-y-auto border border-gray-200 rounded-lg p-2"> | |
| <!-- Words will be loaded here --> | |
| </div> | |
| <div class="mt-3 flex space-x-2"> | |
| <button id="clearWordsBtn" class="flex-1 bg-red-100 text-red-700 py-1 px-3 rounded hover:bg-red-200 transition"> | |
| <i class="fas fa-trash-alt mr-1"></i> Clear All | |
| </button> | |
| <button id="exportWordsBtn" class="flex-1 bg-green-100 text-green-700 py-1 px-3 rounded hover:bg-green-200 transition"> | |
| <i class="fas fa-file-export mr-1"></i> Export | |
| </button> | |
| <button id="importWordsBtn" class="flex-1 bg-blue-100 text-blue-700 py-1 px-3 rounded hover:bg-blue-200 transition"> | |
| <i class="fas fa-file-import mr-1"></i> Import | |
| </button> | |
| </div> | |
| <input type="file" id="importFileInput" class="hidden" accept=".json"> | |
| </div> | |
| </div> | |
| <!-- Learning Modes Section --> | |
| <div class="bg-white rounded-xl shadow-lg p-6"> | |
| <h2 class="text-2xl font-semibold text-indigo-700 mb-4">Learning Modes</h2> | |
| <div class="space-y-4"> | |
| <button id="flashcardsBtn" class="w-full bg-purple-100 text-purple-800 py-3 px-4 rounded-lg hover:bg-purple-200 transition flex items-center justify-between"> | |
| <div> | |
| <i class="fas fa-layer-group mr-2"></i> Flashcards | |
| </div> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| <button id="quizBtn" class="w-full bg-teal-100 text-teal-800 py-3 px-4 rounded-lg hover:bg-teal-200 transition flex items-center justify-between"> | |
| <div> | |
| <i class="fas fa-question-circle mr-2"></i> Multiple Choice Quiz | |
| </div> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| <button id="typingBtn" class="w-full bg-amber-100 text-amber-800 py-3 px-4 rounded-lg hover:bg-amber-200 transition flex items-center justify-between"> | |
| <div> | |
| <i class="fas fa-keyboard mr-2"></i> Typing Practice | |
| </div> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| </div> | |
| <div class="mt-6"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-3">Progress Tracking</h3> | |
| <div class="bg-gray-100 rounded-lg p-4"> | |
| <div class="flex justify-between mb-1"> | |
| <span class="text-sm font-medium text-gray-700">Mastery</span> | |
| <span id="masteryPercent" class="text-sm font-medium text-gray-700">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-300 rounded-full h-2.5"> | |
| <div id="masteryBar" class="bg-indigo-600 h-2.5 rounded-full progress-bar" style="width: 0%"></div> | |
| </div> | |
| <div class="mt-3 grid grid-cols-3 gap-2 text-center"> | |
| <div class="bg-blue-50 p-2 rounded"> | |
| <div class="text-blue-800 font-bold" id="totalWords">0</div> | |
| <div class="text-xs text-blue-600">Total Words</div> | |
| </div> | |
| <div class="bg-green-50 p-2 rounded"> | |
| <div class="text-green-800 font-bold" id="knownWords">0</div> | |
| <div class="text-xs text-green-600">Known</div> | |
| </div> | |
| <div class="bg-red-50 p-2 rounded"> | |
| <div class="text-red-800 font-bold" id="learningWords">0</div> | |
| <div class="text-xs text-red-600">Learning</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Game Display Area --> | |
| <div class="bg-white rounded-xl shadow-lg p-6"> | |
| <div id="gameArea" class="h-full flex flex-col items-center justify-center"> | |
| <div class="text-center text-gray-500"> | |
| <i class="fas fa-book-open text-4xl mb-2"></i> | |
| <p>Select a learning mode to begin</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Flashcards Modal --> | |
| <div id="flashcardsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-xl shadow-2xl w-full max-w-md mx-4"> | |
| <div class="p-4 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 class="text-xl font-semibold text-indigo-700">Flashcards</h3> | |
| <button id="closeFlashcards" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-6"> | |
| <div id="flashcardContainer" class="relative h-64 mb-6"> | |
| <div id="noCardsMessage" class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-exclamation-circle text-3xl mb-3"></i> | |
| <p>No flashcards available. Please add some words first.</p> | |
| </div> | |
| <div id="flashcard" class="card hidden w-full h-full"> | |
| <div class="card-inner w-full h-full"> | |
| <div class="card-front bg-indigo-100 rounded-lg shadow-md flex items-center justify-center cursor-pointer p-4"> | |
| <div class="text-center"> | |
| <p class="text-2xl font-bold text-indigo-800" id="cardFrontText">Word</p> | |
| <p class="text-sm text-indigo-600 mt-2">Click to flip</p> | |
| </div> | |
| </div> | |
| <div class="card-back bg-white rounded-lg shadow-md flex items-center justify-center cursor-pointer p-4 border-2 border-indigo-200"> | |
| <div class="text-center"> | |
| <p class="text-xl font-semibold text-gray-800" id="cardBackMeaning">Meaning</p> | |
| <p class="text-sm text-gray-600 mt-2 italic" id="cardBackExample">Example sentence</p> | |
| <div class="mt-4 flex justify-center space-x-3"> | |
| <button class="knowBtn px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm hover:bg-green-200"> | |
| <i class="fas fa-check mr-1"></i> Know | |
| </button> | |
| <button class="dontKnowBtn px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm hover:bg-red-200"> | |
| <i class="fas fa-times mr-1"></i> Don't Know | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="text-sm text-gray-600"> | |
| Card <span id="currentCard">0</span> of <span id="totalCards">0</span> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="prevCard" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"> | |
| <i class="fas fa-arrow-left"></i> Previous | |
| </button> | |
| <button id="nextCard" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"> | |
| Next <i class="fas fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quiz Modal --> | |
| <div id="quizModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-xl shadow-2xl w-full max-w-md mx-4"> | |
| <div class="p-4 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 class="text-xl font-semibold text-indigo-700">Multiple Choice Quiz</h3> | |
| <button id="closeQuiz" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-6"> | |
| <div id="quizContainer" class="mb-6"> | |
| <div id="noQuizMessage" class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-exclamation-circle text-3xl mb-3"></i> | |
| <p>Not enough words for a quiz. Please add at least 4 words.</p> | |
| </div> | |
| <div id="quizQuestion" class="hidden"> | |
| <div class="bg-indigo-50 rounded-lg p-4 mb-4"> | |
| <p class="text-lg font-medium text-center text-indigo-800" id="quizWord">Word</p> | |
| </div> | |
| <p class="text-sm text-gray-600 mb-4 text-center">What is the correct meaning?</p> | |
| <div class="space-y-3" id="quizOptions"> | |
| <!-- Options will be added here by JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="text-sm text-gray-600"> | |
| Question <span id="currentQuestion">0</span> of <span id="totalQuestions">0</span> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="prevQuestion" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"> | |
| <i class="fas fa-arrow-left"></i> Previous | |
| </button> | |
| <button id="nextQuestion" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"> | |
| Next <i class="fas fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Typing Practice Modal --> | |
| <div id="typingModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-xl shadow-2xl w-full max-w-md mx-4"> | |
| <div class="p-4 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 class="text-xl font-semibold text-indigo-700">Typing Practice</h3> | |
| <button id="closeTyping" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-6"> | |
| <div id="typingContainer" class="mb-6"> | |
| <div id="noTypingMessage" class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-exclamation-circle text-3xl mb-3"></i> | |
| <p>No words available for typing practice. Please add some words first.</p> | |
| </div> | |
| <div id="typingQuestion" class="hidden"> | |
| <div class="bg-indigo-50 rounded-lg p-4 mb-4"> | |
| <p class="text-lg font-medium text-center text-indigo-800" id="typingPrompt">Meaning</p> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm text-gray-600 mb-2">Type the English word:</label> | |
| <input type="text" id="typingAnswer" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" autocomplete="off"> | |
| </div> | |
| <div id="typingFeedback" class="hidden text-center py-2 rounded-lg"></div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="text-sm text-gray-600"> | |
| Word <span id="currentTyping">0</span> of <span id="totalTyping">0</span> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="checkTyping" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"> | |
| <i class="fas fa-check mr-1"></i> Check | |
| </button> | |
| <button id="nextTyping" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 hidden"> | |
| Next <i class="fas fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Default vocabulary with 100 common English words | |
| const defaultVocabulary = [ | |
| // Basic Verbs (1-20) | |
| { english: "be", vietnamese: "thì, là, ở", example: "I am happy" }, | |
| { english: "have", vietnamese: "có", example: "I have a book" }, | |
| { english: "do", vietnamese: "làm", example: "What are you doing?" }, | |
| { english: "say", vietnamese: "nói", example: "She says hello" }, | |
| { english: "go", vietnamese: "đi", example: "Let's go home" }, | |
| { english: "get", vietnamese: "nhận được", example: "I got a gift" }, | |
| { english: "make", vietnamese: "làm, tạo ra", example: "She makes cakes" }, | |
| { english: "know", vietnamese: "biết", example: "I know the answer" }, | |
| { english: "think", vietnamese: "nghĩ", example: "I think so" }, | |
| { english: "take", vietnamese: "lấy", example: "Take your time" }, | |
| { english: "see", vietnamese: "nhìn thấy", example: "I see a bird" }, | |
| { english: "come", vietnamese: "đến", example: "Come here please" }, | |
| { english: "want", vietnamese: "muốn", example: "I want coffee" }, | |
| { english: "look", vietnamese: "nhìn", example: "Look at that!" }, | |
| { english: "use", vietnamese: "sử dụng", example: "Can I use your phone?" }, | |
| { english: "find", vietnamese: "tìm thấy", example: "I found my keys" }, | |
| { english: "give", vietnamese: "đưa cho", example: "Give me the book" }, | |
| { english: "tell", vietnamese: "nói với", example: "Tell me a story" }, | |
| { english: "work", vietnamese: "làm việc", example: "I work at a bank" }, | |
| { english: "call", vietnamese: "gọi", example: "Call me later" }, | |
| // Common Nouns (21-40) | |
| { english: "time", vietnamese: "thời gian", example: "What time is it?" }, | |
| { english: "person", vietnamese: "người", example: "She's a nice person" }, | |
| { english: "year", vietnamese: "năm", example: "Happy new year!" }, | |
| { english: "day", vietnamese: "ngày", example: "Have a nice day" }, | |
| { english: "thing", vietnamese: "thứ, vật", example: "What's that thing?" }, | |
| { english: "man", vietnamese: "đàn ông", example: "That man is tall" }, | |
| { english: "world", vietnamese: "thế giới", example: "Travel around the world" }, | |
| { english: "life", vietnamese: "cuộc sống", example: "Enjoy your life" }, | |
| { english: "hand", vietnamese: "bàn tay", example: "Raise your hand" }, | |
| { english: "part", vietnamese: "phần", example: "Part of the cake" }, | |
| { english: "child", vietnamese: "đứa trẻ", example: "She has two children" }, | |
| { english: "eye", vietnamese: "mắt", example: "My eyes are brown" }, | |
| { english: "woman", vietnamese: "phụ nữ", example: "That woman is my teacher" }, | |
| { english: "place", vietnamese: "nơi chốn", example: "This is a nice place" }, | |
| { english: "week", vietnamese: "tuần", example: "See you next week" }, | |
| { english: "case", vietnamese: "trường hợp", example: "In that case..." }, | |
| { english: "point", vietnamese: "điểm", example: "Good point!" }, | |
| { english: "government", vietnamese: "chính phủ", example: "The government announced..." }, | |
| { english: "company", vietnamese: "công ty", example: "I work for a tech company" }, | |
| { english: "number", vietnamese: "số", example: "Pick a number" }, | |
| // Adjectives (41-60) | |
| { english: "good", vietnamese: "tốt", example: "Good job!" }, | |
| { english: "new", vietnamese: "mới", example: "I bought a new phone" }, | |
| { english: "first", vietnamese: "đầu tiên", example: "My first time" }, | |
| { english: "last", vietnamese: "cuối cùng", example: "The last page" }, | |
| { english: "long", vietnamese: "dài", example: "A long road" }, | |
| { english: "great", vietnamese: "tuyệt vời", example: "Great idea!" }, | |
| { english: "little", vietnamese: "nhỏ", example: "A little dog" }, | |
| { english: "own", vietnamese: "riêng", example: "My own house" }, | |
| { english: "other", vietnamese: "khác", example: "Other people" }, | |
| { english: "old", vietnamese: "cũ, già", example: "An old friend" }, | |
| { english: "right", vietnamese: "đúng, phải", example: "You're right" }, | |
| { english: "big", vietnamese: "lớn", example: "A big city" }, | |
| { english: "high", vietnamese: "cao", example: "High mountains" }, | |
| { english: "different", vietnamese: "khác biệt", example: "Different opinions" }, | |
| { english: "small", vietnamese: "nhỏ", example: "A small gift" }, | |
| { english: "large", vietnamese: "rộng lớn", example: "A large room" }, | |
| { english: "next", vietnamese: "tiếp theo", example: "Next week" }, | |
| { english: "early", vietnamese: "sớm", example: "Early morning" }, | |
| { english: "young", vietnamese: "trẻ", example: "Young people" }, | |
| { english: "important", vietnamese: "quan trọng", example: "Important news" }, | |
| // Common Words (61-80) | |
| { english: "the", vietnamese: "cái, con, người (mạo từ)", example: "The book is interesting" }, | |
| { english: "and", vietnamese: "và", example: "You and me" }, | |
| { english: "that", vietnamese: "đó, kia", example: "That book is mine" }, | |
| { english: "it", vietnamese: "nó", example: "It is raining" }, | |
| { english: "not", vietnamese: "không", example: "I'm not sure" }, | |
| { english: "he", vietnamese: "anh ấy", example: "He is my brother" }, | |
| { english: "as", vietnamese: "như", example: "As you wish" }, | |
| { english: "at", vietnamese: "ở, tại", example: "At home" }, | |
| { english: "by", vietnamese: "bởi, bằng", example: "By bus" }, | |
| { english: "from", vietnamese: "từ", example: "From Vietnam" }, | |
| { english: "or", vietnamese: "hoặc", example: "Tea or coffee?" }, | |
| { english: "one", vietnamese: "một", example: "One dollar" }, | |
| { english: "all", vietnamese: "tất cả", example: "All students" }, | |
| { english: "would", vietnamese: "sẽ", example: "I would like to go" }, | |
| { english: "there", vietnamese: "ở đó", example: "Go there" }, | |
| { english: "their", vietnamese: "của họ", example: "Their house" }, | |
| { english: "what", vietnamese: "cái gì", example: "What is this?" }, | |
| { english: "so", vietnamese: "vì vậy", example: "I'm tired, so I'll rest" }, | |
| { english: "up", vietnamese: "lên", example: "Stand up" }, | |
| { english: "out", vietnamese: "ra ngoài", example: "Get out" }, | |
| // Useful Words (81-100) | |
| { english: "about", vietnamese: "về", example: "Tell me about yourself" }, | |
| { english: "who", vietnamese: "ai", example: "Who is that?" }, | |
| { english: "which", vietnamese: "cái nào", example: "Which one do you want?" }, | |
| { english: "when", vietnamese: "khi nào", example: "When are you coming?" }, | |
| { english: "where", vietnamese: "ở đâu", example: "Where is the station?" }, | |
| { english: "why", vietnamese: "tại sao", example: "Why are you late?" }, | |
| { english: "how", vietnamese: "như thế nào", example: "How does it work?" }, | |
| { english: "some", vietnamese: "một vài", example: "I need some help" }, | |
| { english: "more", vietnamese: "nhiều hơn", example: "I want more time" }, | |
| { english: "most", vietnamese: "hầu hết", example: "Most people agree" }, | |
| { english: "many", vietnamese: "nhiều", example: "Many thanks" }, | |
| { english: "then", vietnamese: "sau đó", example: "We ate, then left" }, | |
| { english: "now", vietnamese: "bây giờ", example: "Do it now" }, | |
| { english: "only", vietnamese: "chỉ", example: "Only one person" }, | |
| { english: "also", vietnamese: "cũng", example: "I also like tea" }, | |
| { english: "very", vietnamese: "rất", example: "Very good" }, | |
| { english: "even", vietnamese: "thậm chí", example: "Even children know" }, | |
| { english: "back", vietnamese: "trở lại", example: "Come back soon" }, | |
| { english: "any", vietnamese: "bất kỳ", example: "Any questions?" }, | |
| { english: "well", vietnamese: "tốt", example: "You did well" } | |
| ]; | |
| // Vocabulary data storage | |
| let vocabulary = JSON.parse(localStorage.getItem('vocabulary')) || defaultVocabulary; | |
| let knownWords = JSON.parse(localStorage.getItem('knownWords')) || []; | |
| // Current game state | |
| let currentFlashcardIndex = 0; | |
| let currentQuizIndex = 0; | |
| let currentTypingIndex = 0; | |
| let quizAnswers = []; | |
| let typingAnswers = []; | |
| // DOM elements | |
| const wordList = document.getElementById('wordList'); | |
| const englishWordInput = document.getElementById('englishWord'); | |
| const vietnameseMeaningInput = document.getElementById('vietnameseMeaning'); | |
| const exampleSentenceInput = document.getElementById('exampleSentence'); | |
| const addWordBtn = document.getElementById('addWordBtn'); | |
| const clearWordsBtn = document.getElementById('clearWordsBtn'); | |
| const exportWordsBtn = document.getElementById('exportWordsBtn'); | |
| const importWordsBtn = document.getElementById('importWordsBtn'); | |
| const importFileInput = document.getElementById('importFileInput'); | |
| // Game mode buttons | |
| const flashcardsBtn = document.getElementById('flashcardsBtn'); | |
| const quizBtn = document.getElementById('quizBtn'); | |
| const typingBtn = document.getElementById('typingBtn'); | |
| // Progress elements | |
| const totalWordsEl = document.getElementById('totalWords'); | |
| const knownWordsEl = document.getElementById('knownWords'); | |
| const learningWordsEl = document.getElementById('learningWords'); | |
| const masteryPercentEl = document.getElementById('masteryPercent'); | |
| const masteryBarEl = document.getElementById('masteryBar'); | |
| // Flashcards modal elements | |
| const flashcardsModal = document.getElementById('flashcardsModal'); | |
| const flashcardContainer = document.getElementById('flashcardContainer'); | |
| const flashcard = document.getElementById('flashcard'); | |
| const noCardsMessage = document.getElementById('noCardsMessage'); | |
| const cardFrontText = document.getElementById('cardFrontText'); | |
| const cardBackMeaning = document.getElementById('cardBackMeaning'); | |
| const cardBackExample = document.getElementById('cardBackExample'); | |
| const currentCardEl = document.getElementById('currentCard'); | |
| const totalCardsEl = document.getElementById('totalCards'); | |
| const prevCardBtn = document.getElementById('prevCard'); | |
| const nextCardBtn = document.getElementById('nextCard'); | |
| const closeFlashcards = document.getElementById('closeFlashcards'); | |
| // Quiz modal elements | |
| const quizModal = document.getElementById('quizModal'); | |
| const quizContainer = document.getElementById('quizContainer'); | |
| const noQuizMessage = document.getElementById('noQuizMessage'); | |
| const quizQuestion = document.getElementById('quizQuestion'); | |
| const quizWord = document.getElementById('quizWord'); | |
| const quizOptions = document.getElementById('quizOptions'); | |
| const currentQuestionEl = document.getElementById('currentQuestion'); | |
| const totalQuestionsEl = document.getElementById('totalQuestions'); | |
| const prevQuestionBtn = document.getElementById('prevQuestion'); | |
| const nextQuestionBtn = document.getElementById('nextQuestion'); | |
| const closeQuiz = document.getElementById('closeQuiz'); | |
| // Typing modal elements | |
| const typingModal = document.getElementById('typingModal'); | |
| const typingContainer = document.getElementById('typingContainer'); | |
| const noTypingMessage = document.getElementById('noTypingMessage'); | |
| const typingQuestion = document.getElementById('typingQuestion'); | |
| const typingPrompt = document.getElementById('typingPrompt'); | |
| const typingAnswer = document.getElementById('typingAnswer'); | |
| const typingFeedback = document.getElementById('typingFeedback'); | |
| const currentTypingEl = document.getElementById('currentTyping'); | |
| const totalTypingEl = document.getElementById('totalTyping'); | |
| const checkTypingBtn = document.getElementById('checkTyping'); | |
| const nextTypingBtn = document.getElementById('nextTyping'); | |
| const closeTyping = document.getElementById('closeTyping'); | |
| // Initialize the app | |
| function init() { | |
| // If no vocabulary in localStorage, save the default | |
| if (!localStorage.getItem('vocabulary')) { | |
| saveVocabulary(); | |
| } | |
| updateWordList(); | |
| updateProgress(); | |
| setupEventListeners(); | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Add word button | |
| addWordBtn.addEventListener('click', addWord); | |
| // Enter key to add word | |
| englishWordInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') addWord(); | |
| }); | |
| vietnameseMeaningInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') addWord(); | |
| }); | |
| // Clear, export, import buttons | |
| clearWordsBtn.addEventListener('click', clearWords); | |
| exportWordsBtn.addEventListener('click', exportWords); | |
| importWordsBtn.addEventListener('click', () => importFileInput.click()); | |
| importFileInput.addEventListener('change', importWords); | |
| // Game mode buttons | |
| flashcardsBtn.addEventListener('click', startFlashcards); | |
| quizBtn.addEventListener('click', startQuiz); | |
| typingBtn.addEventListener('click', startTyping); | |
| // Flashcards events | |
| flashcard.addEventListener('click', flipCard); | |
| prevCardBtn.addEventListener('click', showPreviousCard); | |
| nextCardBtn.addEventListener('click', showNextCard); | |
| closeFlashcards.addEventListener('click', () => flashcardsModal.classList.add('hidden')); | |
| // Quiz events | |
| prevQuestionBtn.addEventListener('click', showPreviousQuestion); | |
| nextQuestionBtn.addEventListener('click', showNextQuestion); | |
| closeQuiz.addEventListener('click', () => quizModal.classList.add('hidden')); | |
| // Typing events | |
| typingAnswer.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') checkTypingAnswer(); | |
| }); | |
| checkTypingBtn.addEventListener('click', checkTypingAnswer); | |
| nextTypingBtn.addEventListener('click', showNextTyping); | |
| closeTyping.addEventListener('click', () => typingModal.classList.add('hidden')); | |
| // Know/Don't know buttons (event delegation) | |
| document.addEventListener('click', function(e) { | |
| if (e.target.classList.contains('knowBtn')) { | |
| markWordAsKnown(true); | |
| } else if (e.target.classList.contains('dontKnowBtn')) { | |
| markWordAsKnown(false); | |
| } | |
| }); | |
| } | |
| // Add a new word to vocabulary | |
| function addWord() { | |
| const englishWord = englishWordInput.value.trim(); | |
| const vietnameseMeaning = vietnameseMeaningInput.value.trim(); | |
| const exampleSentence = exampleSentenceInput.value.trim(); | |
| if (!englishWord || !vietnameseMeaning) { | |
| alert('Please enter both English word and Vietnamese meaning'); | |
| return; | |
| } | |
| // Check if word already exists | |
| if (vocabulary.some(word => word.english.toLowerCase() === englishWord.toLowerCase())) { | |
| alert('This word already exists in your vocabulary list'); | |
| return; | |
| } | |
| const newWord = { | |
| english: englishWord, | |
| vietnamese: vietnameseMeaning, | |
| example: exampleSentence, | |
| added: new Date().toISOString() | |
| }; | |
| vocabulary.push(newWord); | |
| saveVocabulary(); | |
| // Clear inputs | |
| englishWordInput.value = ''; | |
| vietnameseMeaningInput.value = ''; | |
| exampleSentenceInput.value = ''; | |
| // Focus back to English input | |
| englishWordInput.focus(); | |
| // Update UI | |
| updateWordList(); | |
| updateProgress(); | |
| } | |
| // Clear all words | |
| function clearWords() { | |
| if (confirm('Are you sure you want to clear all words? This will reset to default vocabulary.')) { | |
| vocabulary = [...defaultVocabulary]; | |
| knownWords = []; | |
| saveVocabulary(); | |
| updateWordList(); | |
| updateProgress(); | |
| } | |
| } | |
| // Export words to JSON file | |
| function exportWords() { | |
| if (vocabulary.length === 0) { | |
| alert('No words to export'); | |
| return; | |
| } | |
| const data = { | |
| vocabulary: vocabulary, | |
| knownWords: knownWords, | |
| exported: new Date().toISOString() | |
| }; | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `vocabulary-${new Date().toISOString().slice(0, 10)}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Import words from JSON file | |
| function importWords(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| const data = JSON.parse(event.target.result); | |
| if (data.vocabulary && Array.isArray(data.vocabulary)) { | |
| // Merge with existing vocabulary, avoiding duplicates | |
| data.vocabulary.forEach(word => { | |
| if (!vocabulary.some(w => w.english.toLowerCase() === word.english.toLowerCase())) { | |
| vocabulary.push(word); | |
| } | |
| }); | |
| // Update known words if they exist in import | |
| if (data.knownWords && Array.isArray(data.knownWords)) { | |
| data.knownWords.forEach(word => { | |
| if (!knownWords.includes(word) && vocabulary.some(w => w.english === word)) { | |
| knownWords.push(word); | |
| } | |
| }); | |
| } | |
| saveVocabulary(); | |
| updateWordList(); | |
| updateProgress(); | |
| alert(`Successfully imported ${data.vocabulary.length} words`); | |
| } else { | |
| alert('Invalid file format'); | |
| } | |
| } catch (error) { | |
| alert('Error reading file: ' + error.message); | |
| } | |
| // Reset file input | |
| e.target.value = ''; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Save vocabulary to localStorage | |
| function saveVocabulary() { | |
| localStorage.setItem('vocabulary', JSON.stringify(vocabulary)); | |
| localStorage.setItem('knownWords', JSON.stringify(knownWords)); | |
| } | |
| // Update the word list display | |
| function updateWordList() { | |
| if (vocabulary.length === 0) { | |
| wordList.innerHTML = '<p class="text-gray-500 text-center py-4">No words added yet</p>'; | |
| return; | |
| } | |
| wordList.innerHTML = ''; | |
| // Group words by first letter for better organization | |
| const groupedWords = vocabulary.reduce((groups, word) => { | |
| const firstLetter = word.english.charAt(0).toUpperCase(); | |
| if (!groups[firstLetter]) { | |
| groups[firstLetter] = []; | |
| } | |
| groups[firstLetter].push(word); | |
| return groups; | |
| }, {}); | |
| // Sort letters alphabetically | |
| const sortedLetters = Object.keys(groupedWords).sort(); | |
| // Display words by letter groups | |
| sortedLetters.forEach(letter => { | |
| const letterGroup = document.createElement('div'); | |
| letterGroup.className = 'mb-4'; | |
| const letterHeader = document.createElement('div'); | |
| letterHeader.className = 'word-category px-3 py-2 mb-2 font-medium text-indigo-800'; | |
| letterHeader.textContent = letter; | |
| letterGroup.appendChild(letterHeader); | |
| groupedWords[letter].forEach((word, index) => { | |
| const isKnown = knownWords.includes(word.english); | |
| const wordEl = document.createElement('div'); | |
| wordEl.className = `flex justify-between items-center p-2 mb-1 rounded-lg ${isKnown ? 'bg-green-50' : 'bg-white'} border ${isKnown ? 'border-green-200' : 'border-gray-200'}`; | |
| wordEl.innerHTML = ` | |
| <div class="flex-1"> | |
| <div class="font-medium text-gray-800">${word.english}</div> | |
| <div class="text-sm text-gray-600">${word.vietnamese}</div> | |
| ${word.example ? `<div class="text-xs text-gray-500 italic mt-1">"${word.example}"</div>` : ''} | |
| </div> | |
| <div class="flex space-x-1"> | |
| <button class="know-toggle px-2 py-1 text-xs rounded ${isKnown ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}" data-word="${word.english}"> | |
| <i class="fas ${isKnown ? 'fa-check-circle' : 'fa-question-circle'} mr-1"></i> ${isKnown ? 'Known' : 'Learning'} | |
| </button> | |
| ${!defaultVocabulary.some(w => w.english === word.english) ? | |
| `<button class="delete-word px-2 py-1 text-xs bg-red-100 text-red-800 rounded" data-index="${vocabulary.findIndex(w => w.english === word.english)}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button>` : ''} | |
| </div> | |
| `; | |
| letterGroup.appendChild(wordEl); | |
| }); | |
| wordList.appendChild(letterGroup); | |
| }); | |
| // Add event listeners for dynamically created buttons | |
| document.querySelectorAll('.know-toggle').forEach(btn => { | |
| btn.addEventListener('click', toggleKnownWord); | |
| }); | |
| document.querySelectorAll('.delete-word').forEach(btn => { | |
| btn.addEventListener('click', deleteWord); | |
| }); | |
| } | |
| // Toggle word between known and learning | |
| function toggleKnownWord(e) { | |
| const word = e.target.closest('button').getAttribute('data-word'); | |
| const index = knownWords.indexOf(word); | |
| if (index === -1) { | |
| knownWords.push(word); | |
| } else { | |
| knownWords.splice(index, 1); | |
| } | |
| saveVocabulary(); | |
| updateWordList(); | |
| updateProgress(); | |
| } | |
| // Delete a word from vocabulary | |
| function deleteWord(e) { | |
| const index = parseInt(e.target.closest('button').getAttribute('data-index')); | |
| if (index < 0 || index >= vocabulary.length) return; | |
| // Don't allow deleting default words | |
| if (defaultVocabulary.some(w => w.english === vocabulary[index].english)) { | |
| alert('Default words cannot be deleted. Clear all to reset.'); | |
| return; | |
| } | |
| vocabulary.splice(index, 1); | |
| // Remove from known words if it was there | |
| const wordToDelete = vocabulary[index]?.english; | |
| if (wordToDelete) { | |
| const knownIndex = knownWords.indexOf(wordToDelete); | |
| if (knownIndex !== -1) { | |
| knownWords.splice(knownIndex, 1); | |
| } | |
| } | |
| saveVocabulary(); | |
| updateWordList(); | |
| updateProgress(); | |
| } | |
| // Update progress stats | |
| function updateProgress() { | |
| const total = vocabulary.length; | |
| const known = knownWords.length; | |
| const learning = total - known; | |
| const mastery = total > 0 ? Math.round((known / total) * 100) : 0; | |
| totalWordsEl.textContent = total; | |
| knownWordsEl.textContent = known; | |
| learningWordsEl.textContent = learning; | |
| masteryPercentEl.textContent = `${mastery}%`; | |
| masteryBarEl.style.width = `${mastery}%`; | |
| // Change color based on mastery level | |
| if (mastery >= 70) { | |
| masteryBarEl.className = 'bg-green-600 h-2.5 rounded-full progress-bar'; | |
| } else if (mastery >= 40) { | |
| masteryBarEl.className = 'bg-yellow-500 h-2.5 rounded-full progress-bar'; | |
| } else { | |
| masteryBarEl.className = 'bg-red-500 h-2.5 rounded-full progress-bar'; | |
| } | |
| } | |
| // Start flashcards game | |
| function startFlashcards() { | |
| if (vocabulary.length === 0) { | |
| alert('Please add some words first'); | |
| return; | |
| } | |
| currentFlashcardIndex = 0; | |
| updateFlashcard(); | |
| // Show modal | |
| flashcardsModal.classList.remove('hidden'); | |
| } | |
| // Update flashcard display | |
| function updateFlashcard() { | |
| if (vocabulary.length === 0) { | |
| noCardsMessage.classList.remove('hidden'); | |
| flashcard.classList.add('hidden'); | |
| return; | |
| } | |
| noCardsMessage.classList.add('hidden'); | |
| flashcard.classList.remove('hidden'); | |
| const word = vocabulary[currentFlashcardIndex]; | |
| cardFrontText.textContent = word.english; | |
| cardBackMeaning.textContent = word.vietnamese; | |
| cardBackExample.textContent = word.example || 'No example provided'; | |
| // Reset card to front | |
| flashcard.classList.remove('flipped'); | |
| // Update counters | |
| currentCardEl.textContent = currentFlashcardIndex + 1; | |
| totalCardsEl.textContent = vocabulary.length; | |
| // Update button states | |
| prevCardBtn.disabled = currentFlashcardIndex === 0; | |
| nextCardBtn.disabled = currentFlashcardIndex === vocabulary.length - 1; | |
| } | |
| // Flip the flashcard | |
| function flipCard() { | |
| flashcard.classList.toggle('flipped'); | |
| } | |
| // Mark word as known or not known | |
| function markWordAsKnown(isKnown) { | |
| const currentWord = vocabulary[currentFlashcardIndex].english; | |
| const index = knownWords.indexOf(currentWord); | |
| if (isKnown && index === -1) { | |
| knownWords.push(currentWord); | |
| } else if (!isKnown && index !== -1) { | |
| knownWords.splice(index, 1); | |
| } | |
| saveVocabulary(); | |
| updateProgress(); | |
| // Move to next card automatically | |
| if (currentFlashcardIndex < vocabulary.length - 1) { | |
| currentFlashcardIndex++; | |
| updateFlashcard(); | |
| } | |
| } | |
| // Show previous flashcard | |
| function showPreviousCard() { | |
| if (currentFlashcardIndex > 0) { | |
| currentFlashcardIndex--; | |
| updateFlashcard(); | |
| } | |
| } | |
| // Show next flashcard | |
| function showNextCard() { | |
| if (currentFlashcardIndex < vocabulary.length - 1) { | |
| currentFlashcardIndex++; | |
| updateFlashcard(); | |
| } | |
| } | |
| // Start quiz game | |
| function startQuiz() { | |
| if (vocabulary.length < 4) { | |
| alert('You need at least 4 words to start a quiz'); | |
| return; | |
| } | |
| currentQuizIndex = 0; | |
| quizAnswers = []; | |
| updateQuizQuestion(); | |
| // Show modal | |
| quizModal.classList.remove('hidden'); | |
| } | |
| // Update quiz question display | |
| function updateQuizQuestion() { | |
| if (vocabulary.length < 4) { | |
| noQuizMessage.classList.remove('hidden'); | |
| quizQuestion.classList.add('hidden'); | |
| return; | |
| } | |
| noQuizMessage.classList.add('hidden'); | |
| quizQuestion.classList.remove('hidden'); | |
| const currentWord = vocabulary[currentQuizIndex]; | |
| quizWord.textContent = currentWord.english; | |
| // Generate options (1 correct + 3 random incorrect) | |
| const options = [currentWord.vietnamese]; | |
| // Get 3 random incorrect options | |
| while (options.length < 4) { | |
| const randomIndex = Math.floor(Math.random() * vocabulary.length); | |
| if (randomIndex !== currentQuizIndex && !options.includes(vocabulary[randomIndex].vietnamese)) { | |
| options.push(vocabulary[randomIndex].vietnamese); | |
| } | |
| } | |
| // Shuffle options | |
| options.sort(() => Math.random() - 0.5); | |
| // Display options | |
| quizOptions.innerHTML = ''; | |
| options.forEach((option, i) => { | |
| const optionEl = document.createElement('button'); | |
| optionEl.className = 'w-full text-left p-3 bg-gray-100 hover:bg-gray-200 rounded-lg transition'; | |
| optionEl.textContent = `${i + 1}. ${option}`; | |
| optionEl.dataset.option = option; | |
| // Check if this option was already selected | |
| const currentAnswer = quizAnswers[currentQuizIndex]; | |
| if (currentAnswer && currentAnswer.selected === option) { | |
| optionEl.className = currentAnswer.correct ? | |
| 'w-full text-left p-3 bg-green-100 text-green-800 rounded-lg' : | |
| 'w-full text-left p-3 bg-red-100 text-red-800 rounded-lg'; | |
| } | |
| optionEl.addEventListener('click', () => selectQuizOption(optionEl, option, currentWord.vietnamese)); | |
| quizOptions.appendChild(optionEl); | |
| }); | |
| // Update counters | |
| currentQuestionEl.textContent = currentQuizIndex + 1; | |
| totalQuestionsEl.textContent = vocabulary.length; | |
| // Update button states | |
| prevQuestionBtn.disabled = currentQuizIndex === 0; | |
| nextQuestionBtn.disabled = currentQuizIndex === vocabulary.length - 1; | |
| } | |
| // Select a quiz option | |
| function selectQuizOption(optionEl, selectedOption, correctOption) { | |
| // Don't allow changing answer after selection | |
| if (quizAnswers[currentQuizIndex]) return; | |
| const isCorrect = selectedOption === correctOption; | |
| // Update button appearance | |
| optionEl.className = isCorrect ? | |
| 'w-full text-left p-3 bg-green-100 text-green-800 rounded-lg bounce' : | |
| 'w-full text-left p-3 bg-red-100 text-red-800 rounded-lg shake'; | |
| // Store answer | |
| quizAnswers[currentQuizIndex] = { | |
| selected: selectedOption, | |
| correct: isCorrect | |
| }; | |
| // Update known words if answered correctly | |
| const currentWord = vocabulary[currentQuizIndex].english; | |
| if (isCorrect && !knownWords.includes(currentWord)) { | |
| knownWords.push(currentWord); | |
| saveVocabulary(); | |
| updateProgress(); | |
| } | |
| } | |
| // Show previous quiz question | |
| function showPreviousQuestion() { | |
| if (currentQuizIndex > 0) { | |
| currentQuizIndex--; | |
| updateQuizQuestion(); | |
| } | |
| } | |
| // Show next quiz question | |
| function showNextQuestion() { | |
| if (currentQuizIndex < vocabulary.length - 1) { | |
| currentQuizIndex++; | |
| updateQuizQuestion(); | |
| } | |
| } | |
| // Start typing practice | |
| function startTyping() { | |
| if (vocabulary.length === 0) { | |
| alert('Please add some words first'); | |
| return; | |
| } | |
| currentTypingIndex = 0; | |
| typingAnswers = []; | |
| updateTypingQuestion(); | |
| // Show modal | |
| typingModal.classList.remove('hidden'); | |
| } | |
| // Update typing question display | |
| function updateTypingQuestion() { | |
| if (vocabulary.length === 0) { | |
| noTypingMessage.classList.remove('hidden'); | |
| typingQuestion.classList.add('hidden'); | |
| return; | |
| } | |
| noTypingMessage.classList.add('hidden'); | |
| typingQuestion.classList.remove('hidden'); | |
| typingFeedback.classList.add('hidden'); | |
| const currentWord = vocabulary[currentTypingIndex]; | |
| typingPrompt.textContent = currentWord.vietnamese; | |
| typingAnswer.value = ''; | |
| // Update counters | |
| currentTypingEl.textContent = currentTypingIndex + 1; | |
| totalTypingEl.textContent = vocabulary.length; | |
| // Focus on input | |
| typingAnswer.focus(); | |
| // Show check button, hide next button | |
| checkTypingBtn.classList.remove('hidden'); | |
| nextTypingBtn.classList.add('hidden'); | |
| } | |
| // Check typing answer | |
| function checkTypingAnswer() { | |
| const userAnswer = typingAnswer.value.trim().toLowerCase(); | |
| const correctAnswer = vocabulary[currentTypingIndex].english.toLowerCase(); | |
| const isCorrect = userAnswer === correctAnswer; | |
| // Show feedback | |
| typingFeedback.classList.remove('hidden'); | |
| if (isCorrect) { | |
| typingFeedback.className = 'bg-green-100 text-green-800 text-center py-2 rounded-lg bounce'; | |
| typingFeedback.innerHTML = `<i class="fas fa-check-circle mr-2"></i> Correct!`; | |
| // Add to known words if not already there | |
| if (!knownWords.includes(vocabulary[currentTypingIndex].english)) { | |
| knownWords.push(vocabulary[currentTypingIndex].english); | |
| saveVocabulary(); | |
| updateProgress(); | |
| } | |
| } else { | |
| typingFeedback.className = 'bg-red-100 text-red-800 text-center py-2 rounded-lg shake'; | |
| typingFeedback.innerHTML = `<i class="fas fa-times-circle mr-2"></i> Incorrect. The correct answer is "${vocabulary[currentTypingIndex].english}"`; | |
| } | |
| // Store answer | |
| typingAnswers[currentTypingIndex] = { | |
| answer: userAnswer, | |
| correct: isCorrect | |
| }; | |
| // Show next button, hide check button | |
| checkTypingBtn.classList.add('hidden'); | |
| nextTypingBtn.classList.remove('hidden'); | |
| } | |
| // Show next typing question | |
| function showNextTyping() { | |
| if (currentTypingIndex < vocabulary.length - 1) { | |
| currentTypingIndex++; | |
| updateTypingQuestion(); | |
| } else { | |
| // Calculate score if finished all words | |
| const correctCount = typingAnswers.filter(a => a.correct).length; | |
| const score = Math.round((correctCount / vocabulary.length) * 100); | |
| typingFeedback.classList.remove('hidden'); | |
| typingFeedback.className = 'bg-blue-100 text-blue-800 text-center py-2 rounded-lg'; | |
| typingFeedback.innerHTML = ` | |
| <div class="font-medium">Practice complete!</div> | |
| <div>Score: ${score}% (${correctCount} of ${vocabulary.length})</div> | |
| `; | |
| nextTypingBtn.classList.add('hidden'); | |
| } | |
| } | |
| // Initialize the app | |
| init(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ktskhoa/game-learning" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |