├── .editorconfig ├── .gitignore ├── LICENSE.md ├── index.html ├── readme.md ├── styles.css └── script.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*{.css}] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | Icon? 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | # Caches and temp files 12 | temp 13 | tmp 14 | _tmp 15 | .eslintcache 16 | *.sass-cache 17 | 18 | # External libraries/packages/plugins 19 | node_modules 20 | 21 | # -------------------------- 22 | 23 | # Editor 24 | .vscode/settings.json 25 | 26 | # Secrets 27 | secret.js 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jikol1906 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Document 11 | 12 | 13 |
14 | 15 |
16 |
17 | 24 |
25 | 31 | 37 | 38 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |

48 | 49 | 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Sentence Guesser 2 | Sentence Guesser works by taking an English sentence that you provide, translating it to German with DeepL, then returning it to you in the form of a fill-in-the-blanks exercise. 3 | 4 | ## Setup 5 | Request a free DeepL api key from [their website](https://www.deepl.com/). 6 | 7 | Clone the project. 8 | 9 | Create a new file in the project directory called `secret.js`. Add the following code: 10 | 11 | ```js 12 | // global variable 13 | guesser = {} 14 | guesser.apiKey = ''; // your DeepL api key here 15 | ``` 16 | 17 | This workflow prevents you from having to delete your API key from the `script.js` file before committing code. 18 | 19 | ## Usage 20 | Open `index.html` in your browser. 21 | 22 | Begin by typing an English sentence in the input field. 23 | 24 | Click "Translate" to send to DeepL and load the exercise. 25 | 26 | A fill-in-the-blanks German exercise will load with some letters already showing. As you type out your answer, Sentence Guesser will advance focus to the next available character input field. Alternatively you can manually focus on arbitrary character inputs. 27 | 28 | If you are stuck, click "Reveal Random Letter" and the exercise will confirm a letter from the correctly translated answer. 29 | 30 | Once you are ready to evaluate your answer, click "Check". If any characters are incorrectly translated, they will be marked in red (a11y consideration needed here). 31 | 32 | If you want to give up before revealing all of the letters randomly, click "Reveal result", and scroll down to view the correct answer. 33 | 34 | Once you are ready to try a new exercise, click "Try new Sentence", to clear the page. 35 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #fff; 3 | --primary-color: hsl(201, 100%, 17%); 4 | --secondary-color: hsl(201, 100%, 27%); 5 | } 6 | 7 | body { 8 | display: flex; 9 | position: relative; 10 | flex-direction: column; 11 | gap: 1rem; 12 | padding: 4rem 2rem; 13 | background-color: var(--primary-color); 14 | font-family: sans-serif; 15 | } 16 | 17 | .button-wrapper { 18 | display: grid; 19 | grid-auto-flow: column; 20 | grid-auto-columns: 1fr; 21 | justify-content: center; 22 | gap: 2rem; 23 | } 24 | 25 | .tinput { 26 | color: var(--text-color); 27 | } 28 | 29 | ::placeholder { 30 | /* Chrome, Firefox, Opera, Safari 10.1+ */ 31 | color: var(--text-color); 32 | 33 | opacity: .5; 34 | /* Firefox */ 35 | } 36 | 37 | :-ms-input-placeholder { 38 | /* Internet Explorer 10-11 */ 39 | color: var(--text-color); 40 | } 41 | 42 | ::-ms-input-placeholder { 43 | /* Microsoft Edge */ 44 | color: var(--text-color); 45 | } 46 | 47 | .character-input { 48 | background: transparent; 49 | width: 1.8ch; 50 | padding: 5px 0; 51 | text-align: center; 52 | font-size: 3rem; 53 | border: 0; 54 | color: var(--text-color); 55 | border-bottom: 2px solid var(--text-color); 56 | outline: none; 57 | } 58 | 59 | .top-button { 60 | font-family: inherit; 61 | font-size: 2rem; 62 | } 63 | 64 | .top-buttons-container { 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | padding: 0.5rem; 69 | } 70 | 71 | .wrapper { 72 | display: flex; 73 | flex-wrap: wrap; 74 | justify-content: center; 75 | margin: auto; 76 | padding: 4rem 0; 77 | gap: 2rem; 78 | } 79 | 80 | .flex-wrapper { 81 | display: flex; 82 | } 83 | 84 | .word-wrapper { 85 | display: flex; 86 | gap: 0.5rem; 87 | } 88 | 89 | #sentence { 90 | background: transparent; 91 | border: none; 92 | text-align: center; 93 | font-size: 2rem; 94 | outline: none; 95 | resize: none; 96 | } 97 | 98 | #result { 99 | font-size: 3rem; 100 | color: var(--text-color); 101 | display: none; 102 | text-align: center; 103 | } 104 | 105 | .textarea-wrapper { 106 | display: flex; 107 | align-items: center; 108 | gap: 2rem; 109 | flex-direction: column; 110 | } 111 | 112 | #revealresult { 113 | display: none; 114 | } 115 | 116 | /* CSS */ 117 | .btn { 118 | appearance: button; 119 | backface-visibility: hidden; 120 | background-color: var(--secondary-color); 121 | border-radius: 6px; 122 | border-width: 0; 123 | box-shadow: rgba(50, 50, 93, 0.1) 0 0 0 1px inset, 124 | rgba(50, 50, 93, 0.1) 0 2px 5px 0, rgba(0, 0, 0, 0.07) 0 1px 1px 0; 125 | box-sizing: border-box; 126 | color: var(--text-color); 127 | cursor: pointer; 128 | font-family: -apple-system, system-ui, "Segoe UI", Roboto, 129 | "Helvetica Neue", Ubuntu, sans-serif; 130 | font-size: 100%; 131 | height: 44px; 132 | line-height: 1.15; 133 | outline: none; 134 | overflow: hidden; 135 | padding: 0 25px; 136 | position: relative; 137 | text-align: center; 138 | text-transform: none; 139 | transform: translateZ(0); 140 | transition: all 0.2s, box-shadow 0.08s ease-in; 141 | user-select: none; 142 | -webkit-user-select: none; 143 | touch-action: manipulation; 144 | } 145 | 146 | .btn:disabled { 147 | cursor: default; 148 | } 149 | 150 | .btn:focus { 151 | box-shadow: rgba(50, 50, 93, 0.1) 0 0 0 1px inset, 152 | rgba(50, 50, 93, 0.2) 0 6px 15px 0, rgba(0, 0, 0, 0.1) 0 2px 2px 0, 153 | rgba(50, 151, 211, 0.3) 0 0 0 4px; 154 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const wrapper = document.querySelector(".wrapper"); 2 | const topButtons = document.querySelectorAll(".top-button"); 3 | const sentence = document.querySelector("#sentence"); 4 | const translateButton = document.querySelector("#translateButton"); 5 | const trynewButton = document.querySelector("#trynew"); 6 | const reveal = document.querySelector("#revealresult"); 7 | const result = document.querySelector("#result"); 8 | let elms; 9 | let elmsLength; 10 | let currentTabIndex = 1; 11 | let maxTababale; 12 | sentence.focus(); 13 | async function translate(text) { 14 | const apiKey = guesser.apiKey //Your Deepl api key; 15 | const res = await fetch( 16 | `https://api-free.deepl.com/v2/translate?auth_key=${apiKey}&text=${encodeURIComponent( 17 | text 18 | )}&target_lang=de&formality=less` 19 | ); 20 | 21 | const json = await res.json(); 22 | return json.translations[0].text.split(" "); 23 | } 24 | 25 | document.onkeydown = function (e) { 26 | e = e || window.event; 27 | if (e.key === "Backspace" && document.activeElement !== sentence) { 28 | previousInput() 29 | } else if (e.key === "ArrowRight") { 30 | nextInput() 31 | } else if (e.key === "ArrowLeft") { 32 | previousInput() 33 | } 34 | 35 | }; 36 | 37 | function previousInput() { 38 | currentTabIndex = Math.max(1, currentTabIndex - 1); 39 | selectInput(currentTabIndex); 40 | } 41 | 42 | function nextInput() { 43 | currentTabIndex = Math.min(elms.length, currentTabIndex + 1); 44 | selectInput(currentTabIndex); 45 | } 46 | 47 | translateButton.addEventListener("click", async (e) => { 48 | await startNewSentence(sentence.value); 49 | reveal.style.display = "inline"; 50 | translateButton.setAttribute("disabled", true); 51 | }); 52 | 53 | trynewButton.addEventListener("click", (e) => { 54 | sentence.removeAttribute("disabled"); 55 | sentence.value = ""; 56 | sentence.focus(); 57 | translateButton.removeAttribute("disabled"); 58 | wrapper.innerHTML = ""; 59 | result.style.display = ""; 60 | reveal.style.display = ""; 61 | setResult([""]); 62 | }); 63 | 64 | reveal.addEventListener("click", (_) => { 65 | result.style.display = "block"; 66 | }); 67 | 68 | topButtons.forEach((b) => { 69 | b.addEventListener("mousedown", (e) => { 70 | e.preventDefault(); 71 | }); 72 | }); 73 | 74 | function selectInput(tabindex) { 75 | document.querySelector(`input[tabindex='${tabindex}']`)?.focus(); 76 | } 77 | 78 | function insertWord(word) { 79 | const wordWrapper = document.createElement("div"); 80 | wordWrapper.classList.add("word-wrapper"); 81 | 82 | const revealedLetterIndex = getRandomInt(0, word.length); 83 | 84 | [...word].forEach((letter, i) => { 85 | const input = document.createElement("input"); 86 | input.classList.add("character-input"); 87 | input.setAttribute("data-letter", letter); 88 | input.setAttribute("maxlength", 1); 89 | input.setAttribute("type", "text"); 90 | const isPunctuation = !/[a-zäöüß0-9]/i.test(letter); 91 | if (revealedLetterIndex === i || isPunctuation) { 92 | insertLetter(input, letter, isPunctuation); 93 | } 94 | wordWrapper.append(input); 95 | }); 96 | 97 | wrapper.append(wordWrapper); 98 | } 99 | 100 | function insertLetter(input, letter, isPunctuation) { 101 | input.setAttribute("disabled", true); 102 | input.style.color = "white"; 103 | 104 | input.value = letter; 105 | if (isPunctuation) { 106 | input.style.border = "none"; 107 | input.style.background = "transparent"; 108 | } 109 | } 110 | 111 | const handleInput = (e, elms) => { 112 | if (/[a-zäöüß0-9]/i.test(e.target.value)) { 113 | currentTabIndex = Math.min(elmsLength + 1, currentTabIndex + 1); 114 | selectInput(currentTabIndex); 115 | } 116 | }; 117 | 118 | const handleFocus = (e) => { 119 | if (e.target.value !== "") { 120 | e.target.select(); 121 | } 122 | currentTabIndex = +e.target.getAttribute("tabIndex"); 123 | }; 124 | 125 | function setTabIndexes() { 126 | document.querySelectorAll(".wrapper input").forEach((i) => { 127 | i.removeAttribute("tabIndex"); 128 | i.removeEventListener("focus", handleFocus); 129 | }); 130 | elms = document.querySelectorAll(".wrapper input:not(:disabled)"); 131 | elmsLength = elms.length; 132 | maxTababale = elms.length; 133 | 134 | elms.forEach((input, i) => { 135 | input.tabIndex = i + 1; 136 | }); 137 | 138 | elms.forEach((i) => { 139 | i.addEventListener("input", handleInput); 140 | i.addEventListener("focus", handleFocus); 141 | }); 142 | 143 | elms[0].focus(); 144 | } 145 | 146 | function getRandomInt(min, max) { 147 | min = Math.ceil(min); 148 | max = Math.floor(max); 149 | return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive 150 | } 151 | 152 | function getWords() { 153 | const words = Array.from(document.querySelectorAll(".word-wrapper")); 154 | 155 | let sentence = ""; 156 | 157 | words.forEach((w) => { 158 | w.querySelectorAll("input").forEach((i) => { 159 | sentence += i.value; 160 | }); 161 | 162 | sentence += " "; 163 | }); 164 | 165 | console.log(sentence); 166 | } 167 | 168 | function setResult(words) { 169 | document.querySelector("#result").textContent = words.join(" "); 170 | } 171 | 172 | function revealLetter() { 173 | const inputs = Array.from( 174 | wrapper.querySelectorAll("input:not(:disabled)") 175 | ).filter((i) => i.value === ""); 176 | const randomInt = getRandomInt(0, inputs.length); 177 | const randomInput = inputs[randomInt]; 178 | const inputLetter = randomInput.getAttribute("data-letter"); 179 | insertLetter(randomInput, inputLetter, false); 180 | setTabIndexes(); 181 | } 182 | 183 | function checkWords() { 184 | wrapper.querySelectorAll("input").forEach((i) => { 185 | const [value, expectedValue] = [ 186 | i.value.toLocaleLowerCase(), 187 | i.getAttribute("data-letter").toLocaleLowerCase(), 188 | ]; 189 | if (!i.disabled) { 190 | if (value !== expectedValue && value !== "") { 191 | i.style.borderColor = "red"; 192 | i.style.color = "red"; 193 | } else if (value === expectedValue) { 194 | i.style.borderColor = "lightgreen"; 195 | i.style.color = "lightgreen"; 196 | } 197 | } 198 | }); 199 | } 200 | 201 | async function startNewSentence(theSentence) { 202 | const words = await translate(theSentence); 203 | sentence.setAttribute("disabled", true); 204 | words.forEach((w) => insertWord(w)); 205 | setResult(words); 206 | setTabIndexes(); 207 | } 208 | --------------------------------------------------------------------------------