├── resources ├── images │ ├── logo.png │ ├── favicon.ico │ ├── sprite.svg │ └── symbol-defs.svg ├── js │ ├── views │ │ ├── View.js │ │ ├── Modal.js │ │ ├── Restart.js │ │ ├── Counter.js │ │ ├── Input.js │ │ ├── Caret.js │ │ ├── Result.js │ │ ├── Chart.js │ │ ├── Config.js │ │ ├── History.js │ │ ├── WordsWrapper.js │ │ └── Stats.js │ ├── helpers.js │ ├── model.js │ └── controller.js └── css │ └── style.css ├── LICENSE ├── README.md ├── words ├── english.json └── quotes │ └── english.json └── index.html /resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazzon/Rocket-type/HEAD/resources/images/logo.png -------------------------------------------------------------------------------- /resources/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prazzon/Rocket-type/HEAD/resources/images/favicon.ico -------------------------------------------------------------------------------- /resources/js/views/View.js: -------------------------------------------------------------------------------- 1 | export default class View { 2 | _getWordOfType(index) { 3 | return document.querySelector(`.word:nth-of-type(${index})`); 4 | } 5 | 6 | _clearClass(nodeList, classNames) { 7 | nodeList.forEach((node) => { 8 | classNames.forEach((className) => { 9 | node.classList.remove(className); 10 | }); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/js/views/Modal.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Modal extends View { 4 | _parentElement = document.querySelector(".modal"); 5 | _overlay = document.querySelector(".overlay"); 6 | _closeModalBtn = document.querySelector(".btn--close-modal"); 7 | 8 | constructor() { 9 | super(); 10 | 11 | this._overlay.addEventListener("click", () => this.hideModal()); 12 | this._closeModalBtn.addEventListener("click", () => this.hideModal()); 13 | } 14 | 15 | showModal() { 16 | this._parentElement.classList.remove("hidden"); 17 | this._overlay.classList.remove("hidden"); 18 | } 19 | 20 | hideModal() { 21 | this._parentElement.classList.add("hidden"); 22 | this._overlay.classList.add("hidden"); 23 | } 24 | } 25 | 26 | export default new Modal(); 27 | -------------------------------------------------------------------------------- /resources/js/views/Restart.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Restart extends View { 4 | _parentElement = document.querySelector(".restart-container"); 5 | _startAgain = document.querySelector(".start-again"); 6 | _nextTest = document.querySelector(".next-test"); 7 | _restartTest = document.querySelector(".restart"); 8 | 9 | addHandlerRestart(handler) { 10 | this._parentElement.addEventListener("click", function (e) { 11 | const btn = e.target.closest(".restart-btn"); 12 | 13 | if (!btn) return; 14 | 15 | handler(btn.dataset.option); 16 | }); 17 | } 18 | 19 | showRestartBtn() { 20 | this._startAgain.classList.add("hidden"); 21 | this._nextTest.classList.remove("hidden"); 22 | this._restartTest.classList.remove("hidden"); 23 | } 24 | 25 | hideRestartBtn() { 26 | this._startAgain.classList.remove("hidden"); 27 | this._nextTest.classList.add("hidden"); 28 | this._restartTest.classList.add("hidden"); 29 | } 30 | } 31 | 32 | export default new Restart(); 33 | -------------------------------------------------------------------------------- /resources/js/views/Counter.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Counter extends View { 4 | _parentElement = document.querySelector(".counter"); 5 | _counterId; 6 | 7 | setTimeCounter(length) { 8 | this._parentElement.textContent = `${length}`; 9 | } 10 | 11 | setCounter(length, counter = 0) { 12 | this._parentElement.textContent = `${counter}/${length}`; 13 | } 14 | 15 | addHandlerTimer(length, handler) { 16 | if (this._counterId) return; 17 | 18 | var counter = length; 19 | 20 | this._counterId = setInterval(() => { 21 | counter--; 22 | 23 | this._parentElement.textContent = `${counter}`; 24 | 25 | if (counter === 0) { 26 | clearInterval(this._counterId); 27 | 28 | this._counterId = null; 29 | 30 | return handler(); 31 | } 32 | }, 1000); 33 | } 34 | 35 | resetCounter() { 36 | clearInterval(this._counterId); 37 | 38 | this._counterId = null; 39 | } 40 | } 41 | 42 | export default new Counter(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Praise Ogunleye 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 | -------------------------------------------------------------------------------- /resources/js/helpers.js: -------------------------------------------------------------------------------- 1 | export const getText = async function (mode, type) { 2 | try { 3 | if (mode === "quote") { 4 | const response = await fetch("/words/quotes/english.json"); 5 | 6 | const data = await response.json(); 7 | 8 | const filtered = (type !== "all" 9 | ? data.quotes.filter((q) => q.length === type) 10 | : data.quotes); 11 | 12 | const quote = filtered[Math.floor(Math.random() * filtered.length)]; 13 | 14 | return quote.text.split(" "); 15 | } else if (mode === "words") { 16 | const response = await fetch("/words/english.json"); 17 | 18 | const data = await response.json(); 19 | 20 | const words = [...data.words] 21 | .sort(() => 0.5 - Math.random()) 22 | .slice(0, type); 23 | 24 | return words; 25 | } else { 26 | const response = await fetch("/words/english.json"); 27 | 28 | const data = await response.json(); 29 | 30 | const words = [...data.words] 31 | .sort(() => 0.5 - Math.random()) 32 | .slice(0, 1000); 33 | 34 | return words; 35 | } 36 | } catch (error) {} 37 | }; -------------------------------------------------------------------------------- /resources/js/views/Input.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Input extends View { 4 | _parentElement = document.querySelector("#words-input"); 5 | 6 | constructor() { 7 | super(); 8 | 9 | this._parentElement.focus(); 10 | 11 | window.addEventListener("keydown", (e) => { 12 | if (e.code === "Tab" || e.code === "Enter") { 13 | return; 14 | } 15 | 16 | this._parentElement.focus(); 17 | }); 18 | } 19 | 20 | addHandlerInput(handler) { 21 | this._parentElement.addEventListener("input", (e) => { 22 | this._formatInput(); 23 | 24 | handler(this.inputArr, e.inputType); 25 | }); 26 | } 27 | 28 | get inputArr() { 29 | return this._parentElement.value.split(" "); 30 | } 31 | 32 | clearInput() { 33 | this._parentElement.value = ""; 34 | } 35 | 36 | disableInput() { 37 | this._parentElement.disabled = true; 38 | } 39 | 40 | enableInput() { 41 | this._parentElement.disabled = false; 42 | } 43 | 44 | focusInput() { 45 | document.activeElement.blur(); 46 | 47 | this._parentElement.focus(); 48 | } 49 | 50 | _formatInput() { 51 | this._parentElement.value = this._parentElement.value 52 | .trimStart() 53 | .replace(/\s\s+/g, " "); 54 | } 55 | } 56 | 57 | export default new Input(); 58 | -------------------------------------------------------------------------------- /resources/js/views/Caret.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Caret extends View { 4 | _parentElement = document.querySelector(".caret"); 5 | _wordsWrapper = document.querySelector(".words-wrapper"); 6 | 7 | moveCaret(inputArr) { 8 | const currWord = this._getWordOfType(inputArr.length); 9 | 10 | if (!currWord) return; 11 | 12 | const currLetter = currWord?.childNodes[inputArr.slice(-1)[0].length - 1]; 13 | 14 | const scroll = currWord.offsetTop - currWord.offsetHeight - 20; 15 | 16 | const maxScroll = this._wordsWrapper.scrollHeight - this._wordsWrapper.clientHeight - 1; 17 | 18 | const scrollOffset = Math.min(Math.max(scroll, 0), maxScroll); 19 | 20 | this._wordsWrapper.scrollTo({ top: scrollOffset, behavior: "smooth"}); 21 | 22 | // this._wordsWrapper.style.transform = `translateY(-${scrollOffset}px)`; 23 | 24 | const position = { 25 | top: currLetter 26 | ? currLetter.offsetTop - scrollOffset 27 | : currWord.offsetTop - scrollOffset, 28 | left: currLetter ? currLetter.offsetLeft : currWord.offsetLeft, 29 | width: currLetter ? currLetter.offsetWidth : 0, 30 | }; 31 | 32 | this._parentElement.style.top = `${position.top}px`; 33 | 34 | this._parentElement.style.left = `${position.left + position.width}px`; 35 | } 36 | 37 | hideCaret() { 38 | this._parentElement.classList.add("hide"); 39 | } 40 | 41 | showCaret() { 42 | this._parentElement.classList.remove("hide"); 43 | this.moveCaret([""]); 44 | } 45 | 46 | startAnimation() { 47 | this._parentElement.classList.add("animate"); 48 | } 49 | 50 | stopAnimation() { 51 | this._parentElement.classList.remove("animate"); 52 | } 53 | } 54 | 55 | export default new Caret(); 56 | -------------------------------------------------------------------------------- /resources/js/views/Result.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | import WordsWrapper from "./WordsWrapper.js"; 3 | 4 | class Result extends View { 5 | _parentElement = document.querySelector(".result-container"); 6 | _configEl = document.querySelector(".config-container"); 7 | _resultWrapper = document.querySelector(".result-wrapper"); 8 | _wordsWrapper = document.querySelector(".words-wrapper"); 9 | _wpmEl = document.querySelector("#wpm"); 10 | _accuracyEl = document.querySelector("#accuracy"); 11 | _timeEl = document.querySelector("#time"); 12 | _testValue = document.querySelector(".test-value"); 13 | _typeValue = document.querySelector(".type-value"); 14 | _rawValue = document.querySelector(".raw-value"); 15 | _characterValue = document.querySelector(".character-value"); 16 | 17 | showResult(result) { 18 | this._parentElement.classList.remove("hidden"); 19 | 20 | this._resultWrapper.classList.remove("hidden"); 21 | 22 | this._configEl.classList.add("hidden"); 23 | 24 | this._wordsWrapper.classList.add("hidden"); 25 | 26 | // this._wpmEl.textContent = result.wpm.toFixed(2); 27 | // this._accuracyEl.textContent = result.accuracy.toFixed(2) + "%"; 28 | // this._timeEl.textContent = result.time.toFixed(2); 29 | 30 | // round to nearest integer 31 | this._wpmEl.textContent = Math.round(result.wpm); 32 | 33 | this._accuracyEl.textContent = Math.round(result.acc) + "%"; 34 | 35 | this._timeEl.textContent = Math.round(result.time); 36 | 37 | // this._parentElement.scrollIntoView({ behavior: "smooth" }); 38 | 39 | 40 | this._testValue.textContent = result.mode; 41 | 42 | this._typeValue.textContent = result.type; 43 | 44 | this._rawValue.textContent = Math.round(result.raw); 45 | 46 | this._characterValue.textContent = result.letterCount; 47 | } 48 | 49 | hideResult() { 50 | this._parentElement.classList.add("hidden"); 51 | 52 | this._configEl.classList.remove("hidden"); 53 | 54 | this._resultWrapper.classList.add("hidden"); 55 | 56 | this._wordsWrapper.classList.remove("hidden"); 57 | } 58 | } 59 | 60 | export default new Result(); 61 | -------------------------------------------------------------------------------- /resources/js/views/Chart.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | const myChart = document.getElementById("myChart"); 3 | 4 | class ChartClass extends View { 5 | _parentElement = document.getElementById("myChart"); 6 | _correct = document.querySelector(".detail-correct"); 7 | _errors = document.querySelector(".detail-errors"); 8 | _extra = document.querySelector(".detail-extra"); 9 | _missed = document.querySelector(".detail-missed"); 10 | _chart; 11 | 12 | displayChart(result) { 13 | if (this._chart) this._chart.destroy(); 14 | 15 | const data = [ 16 | result.correctLetters, 17 | result.errorCount, 18 | result.extraLetters, 19 | result.missedLetters, 20 | ]; 21 | 22 | this._chart = new Chart(myChart, { 23 | type: "pie", 24 | data: { 25 | labels: ["Correct", "Incorrect", "Extra", "Missed"], 26 | datasets: [ 27 | { 28 | label: "My First Dataset", 29 | data, 30 | backgroundColor: [ 31 | "#3faa67", 32 | "#ff6384", 33 | // "#c33c57", 34 | "#36a2eb", 35 | // "#7a2335", 36 | "#ffcd56", 37 | // "#646669", 38 | ], 39 | hoverOffset: 10, 40 | }, 41 | ], 42 | }, 43 | options: { 44 | borderRadius: 5, 45 | cutout: "80%", 46 | borderColor: "#0b0a0e", 47 | rotation: 180, 48 | layout: { 49 | padding: 15, 50 | }, 51 | plugins: { 52 | tooltip: { 53 | enabled: false, 54 | }, 55 | legend: { 56 | display: false, 57 | }, 58 | }, 59 | }, 60 | }); 61 | 62 | this._correct.textContent = result.correctLetters; 63 | this._errors.textContent = result.errorCount; 64 | this._extra.textContent = result.extraLetters; 65 | this._missed.textContent = result.missedLetters; 66 | } 67 | } 68 | 69 | export default new ChartClass(); 70 | -------------------------------------------------------------------------------- /resources/js/views/Config.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Config extends View { 4 | _parentElement = document.querySelector(".configs"); 5 | _counterEl = document.querySelector(".counter"); 6 | _dividerEl = document.querySelector(".divider"); 7 | 8 | addHandlerConfig(handler) { 9 | this._parentElement.addEventListener("click", (e) => { 10 | const btn = e.target.closest(".mode-button, .type-button"); 11 | 12 | if (!btn) return; 13 | 14 | if (btn.classList.contains("active")) return; 15 | 16 | if (btn.dataset.type) { 17 | this.selectActiveType(btn.dataset.type); 18 | 19 | return handler({ ...btn.dataset }); 20 | } 21 | 22 | const mode = btn.dataset.mode; 23 | 24 | const type = this._getActiveModeType(mode); 25 | 26 | this.selectActiveMode(mode); 27 | 28 | return handler({ ...btn.dataset, type }); 29 | }); 30 | } 31 | 32 | selectActiveMode(mode) { 33 | this._removeActiveClass(".mode-button"); 34 | 35 | this._removeActiveClass(".type"); 36 | 37 | this._addActiveClass(`[data-mode="${mode}"]`); 38 | 39 | this._addActiveClass(`.${mode}-type`); 40 | } 41 | 42 | selectActiveType(type) { 43 | const mode = this._getActiveMode(); 44 | 45 | this._removeActiveClass(`.${mode}-type .type-button`); 46 | 47 | this._addActiveClass(`.${mode}-type [data-type="${type}"]`); 48 | } 49 | 50 | _removeActiveClass(query) { 51 | const el = this._parentElement.querySelector(`${query}.active`); 52 | 53 | el.classList.remove("active"); 54 | } 55 | 56 | _addActiveClass(query) { 57 | const el = this._parentElement.querySelector(query); 58 | 59 | el.classList.add("active"); 60 | } 61 | 62 | _getActiveMode() { 63 | return this._parentElement.querySelector(".mode-button.active").dataset.mode; 64 | } 65 | 66 | _getActiveModeType(mode) { 67 | return this._parentElement.querySelector(`.${mode}-type .type-button.active`) 68 | .dataset.type; 69 | } 70 | 71 | hideConfig() { 72 | this._parentElement.classList.add("hide"); 73 | 74 | this._counterEl.classList.add("expand"); 75 | 76 | this._dividerEl.classList.add("hide"); 77 | } 78 | 79 | showConfig() { 80 | this._parentElement.classList.remove("hide"); 81 | 82 | this._counterEl.classList.remove("expand"); 83 | 84 | this._dividerEl.classList.remove("hide"); 85 | } 86 | } 87 | 88 | export default new Config(); 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rocket Type 2 | 3 | Rocket type is a sleek and intuitive web application designed for typing enthusiasts who want to hone their skills and track their progress effortlessly. 4 | 5 | ## Table of Contents 6 | - [Features](#features) 7 | - [Live Demo](#live-demo) 8 | - [Technical Stack](#technical-stack) 9 | - [MVC Architecture](#mvc-architecture) 10 | - [Contribution](#contribution) 11 | - [License](#license) 12 | - [Feedback and Support](#feedback-and-support) 13 | 14 | ## Features 15 | 16 | 1. **Diverse Typing Modes:** 17 | Rocket type offers a range of modes to suit every typist's preference, including: 18 | - Timed challenges, 19 | - Random word sequences, 20 | - Quotes challenges, and more 21 | 22 | 2. **Progress Tracking:** 23 | The application saves your typing history, providing valuable insights into your improvement over time. Track your performance and celebrate your achievements. 24 | 25 | 3. **Sleek and Intuitive Design:** 26 | Rocket type is crafted with a user-friendly interface, ensuring an enjoyable typing experience. Focus on improving your typing skills without any distractions. 27 | 28 | 4. **Responsive and Accessible:** 29 | Rocket type is designed to be responsive, making it accessible across various devices. Whether you're on a desktop, tablet, or smartphone, Rocket type adapts to your screen size. 30 | 31 | ## Live Demo 32 | 33 | Check out the live demo of Rocket type: [Demo](https://rocket-type.netlify.app) 34 | 35 | ## Technical Stack 36 | 37 | Rocket type is built using the following technologies: 38 | - HTML 39 | - CSS 40 | - JavaScript 41 | 42 | ## MVC Architecture 43 | 44 | Rocket type follows the Model-View-Controller (MVC) architecture pattern. This separation of concerns enhances maintainability, scalability, and modularity of the application. It provides a clear structure for both development and ongoing improvements. 45 | 46 | ## Contribution 47 | 48 | We welcome contributions from the community to make Rocket type even better! If you'd like to contribute, please follow these steps: 49 | 1. Fork the repository. 50 | 2. Create a new branch for your feature or bug fix. 51 | 3. Make your changes and submit a pull request. 52 | 4. Provide a clear and concise description of your changes. 53 | 54 | ## License 55 | 56 | Rocket type is licensed under the [MIT License](LICENSE). Feel free to use, modify, and distribute the code as per the terms of the license. 57 | 58 | ## Feedback and Support 59 | 60 | If you have any feedback, suggestions, or issues, please [open an issue](https://github.com/yourusername/rocket-type/issues). We appreciate your input and aim to make Rocket type a valuable tool for typing enthusiasts. 61 | 62 | Happy typing with Rocket type! 🚀 63 | -------------------------------------------------------------------------------- /resources/js/model.js: -------------------------------------------------------------------------------- 1 | import { getText } from "./helpers.js"; 2 | 3 | export const state = { 4 | words: [], 5 | config: { 6 | mode: "words", 7 | type: 10, 8 | historyOption: "recent", 9 | }, 10 | stats: { 11 | testStarted: 0, 12 | testCompleted: 0, 13 | timeTyping: 0, 14 | }, 15 | isTestStarted: false, 16 | time: {}, 17 | testResult: {}, 18 | history: [], 19 | }; 20 | 21 | const init = function () { 22 | const history = localStorage.getItem("history"); 23 | 24 | const config = localStorage.getItem("config"); 25 | 26 | const stats = localStorage.getItem("stats"); 27 | 28 | if (history) state.history = JSON.parse(history); 29 | 30 | if (config) state.config = JSON.parse(config); 31 | 32 | if (stats) state.stats = JSON.parse(stats); 33 | }; 34 | init(); 35 | 36 | export function updateConfig(config) { 37 | state.config = { ...state.config, ...config }; 38 | 39 | localStorage.setItem("config", JSON.stringify(state.config)); 40 | } 41 | 42 | export const loadText = async function () { 43 | try { 44 | state.words = await getText(state.config.mode, state.config.type); 45 | } catch (error) { 46 | throw error; 47 | } 48 | }; 49 | 50 | export function resetTest() { 51 | state.time = {}; 52 | state.testResult = {}; 53 | state.isTestStarted = false; 54 | } 55 | 56 | export function startTest() { 57 | state.isTestStarted = true; 58 | state.time.timeStarted = Date.now(); 59 | state.stats.testStarted++; 60 | } 61 | 62 | export function endTest() { 63 | state.isTestStarted = false; 64 | state.time.timeStopped = Date.now(); 65 | state.time.timeElapsed = state.time.timeStopped - state.time.timeStarted; 66 | state.stats.timeTyping += state.time.timeElapsed; 67 | state.stats.testCompleted++; 68 | } 69 | 70 | export function updateResult(result) { 71 | const timeInMinutes = state.time.timeElapsed / 60000; 72 | const totalWordCount = result.correctLetters + result.errorCount; // including corrected errors 73 | const correctLetterCount = result.correctLetters + result.wordCount - 1; // including spaces 74 | 75 | const wpm = correctLetterCount / 5 / timeInMinutes; 76 | const raw = result.letterCount / 5 / timeInMinutes; 77 | const acc = result.correctLetters / totalWordCount * 100; 78 | const time = state.time.timeElapsed / 1000; // in seconds 79 | 80 | state.testResult = { 81 | wpm, 82 | raw, 83 | acc, 84 | time, 85 | ...state.config, 86 | ...result, 87 | id: state.history.length, 88 | timeElapse: state.time, 89 | }; 90 | 91 | state.history.unshift(state.testResult); 92 | 93 | localStorage.setItem("history", JSON.stringify(state.history)); 94 | 95 | localStorage.setItem("stats", JSON.stringify(state.stats)); 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/views/History.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class History extends View { 4 | _parentElement = document.querySelector(".history-wrapper"); 5 | _historyOptions = document.querySelector(".history-options-container"); 6 | _historyContainer = document.querySelector(".history-container"); 7 | 8 | addHistoryOptionHandler(handler) { 9 | this._historyOptions.addEventListener("click", (e) => { 10 | const btn = e.target.closest(".history-option"); 11 | 12 | if (!btn) return; 13 | 14 | if (btn.classList.contains("active")) return; 15 | 16 | this._historyOptions.querySelector(".active").classList.remove("active"); 17 | 18 | btn.classList.add("active"); 19 | 20 | return handler(btn.dataset.option); 21 | }); 22 | } 23 | 24 | addHistoryDetailHandler(handler) { 25 | this._historyContainer.addEventListener("click", (e) => { 26 | const btn = e.target.closest(".history-detail"); 27 | 28 | if (!btn) return; 29 | 30 | handler(btn.dataset.id); 31 | }); 32 | } 33 | 34 | selectActiveOption(option) { 35 | this._activeOption.classList.remove("active"); 36 | 37 | const modeButton = document.querySelector(`[data-option='${option}']`); 38 | 39 | modeButton.classList.add("active"); 40 | } 41 | 42 | DisplayHistoryOption(history, words) { 43 | const active = this._activeOption.dataset.option; 44 | 45 | // Render or update container based on active option 46 | 47 | if (active === "inputHistory") return this._updateContainer(words); 48 | 49 | if (active === "recent") return this._renderHistory(history.slice(0, 20)); 50 | 51 | const sorted = [...history].sort((a, b) => b.wpm - a.wpm); 52 | 53 | this._renderHistory(sorted.slice(0, 20)); 54 | } 55 | 56 | get _activeOption() { 57 | const active = this._parentElement.querySelector(".history-option.active"); 58 | 59 | return active; 60 | } 61 | 62 | _renderHistory(histories) { 63 | this._historyContainer.innerHTML = ""; 64 | 65 | histories.forEach((history) => { 66 | const stringEl = ` 67 |
68 |
69 |
wpm
70 |
${Math.round(history.wpm)}
71 |
72 |
73 |
accuracy
74 |
${Math.round(history.acc)}%
75 |
76 |
77 |
${history.mode}
78 |
${history.type}
79 |
80 |
81 |
${this._formatTime( 82 | history.timeElapse.timeStopped 83 | )}
84 |
85 |
86 | `; 87 | 88 | this._historyContainer.insertAdjacentHTML("beforeend", stringEl); 89 | }); 90 | } 91 | 92 | _updateContainer(update) { 93 | const wordHistory = `
${update}
`; 94 | 95 | this._historyContainer.innerHTML = ""; 96 | 97 | this._historyContainer.insertAdjacentHTML("beforeend", wordHistory); 98 | } 99 | 100 | _formatTime(timeInMilliseconds) { 101 | const seconds = Math.round((Date.now() - timeInMilliseconds) / 1000); 102 | 103 | if (seconds < 1) return "now"; 104 | 105 | if (seconds < 60) return seconds + "s ago"; // seconds 106 | 107 | if (seconds < 3600) return Math.floor(seconds / 60) + "m ago"; // minutes 108 | 109 | if (seconds < 86400) return Math.floor(seconds / 3600) + "h ago"; // hours 110 | 111 | return Math.floor(seconds / 86400) + "d ago"; // days 112 | } 113 | } 114 | 115 | export default new History(); 116 | -------------------------------------------------------------------------------- /words/english.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "english", 3 | "words": [ 4 | "the", 5 | "be", 6 | "of", 7 | "and", 8 | "a", 9 | "to", 10 | "in", 11 | "he", 12 | "have", 13 | "it", 14 | "that", 15 | "for", 16 | "they", 17 | "I", 18 | "with", 19 | "as", 20 | "not", 21 | "on", 22 | "she", 23 | "at", 24 | "by", 25 | "this", 26 | "we", 27 | "you", 28 | "do", 29 | "but", 30 | "from", 31 | "or", 32 | "which", 33 | "one", 34 | "would", 35 | "all", 36 | "will", 37 | "there", 38 | "say", 39 | "who", 40 | "make", 41 | "when", 42 | "can", 43 | "more", 44 | "if", 45 | "no", 46 | "man", 47 | "out", 48 | "other", 49 | "so", 50 | "what", 51 | "time", 52 | "up", 53 | "go", 54 | "about", 55 | "than", 56 | "into", 57 | "could", 58 | "state", 59 | "only", 60 | "new", 61 | "year", 62 | "some", 63 | "take", 64 | "come", 65 | "these", 66 | "know", 67 | "see", 68 | "use", 69 | "get", 70 | "like", 71 | "then", 72 | "first", 73 | "any", 74 | "work", 75 | "now", 76 | "may", 77 | "such", 78 | "give", 79 | "over", 80 | "think", 81 | "most", 82 | "even", 83 | "find", 84 | "day", 85 | "also", 86 | "after", 87 | "way", 88 | "many", 89 | "must", 90 | "look", 91 | "before", 92 | "great", 93 | "back", 94 | "through", 95 | "long", 96 | "where", 97 | "much", 98 | "should", 99 | "well", 100 | "people", 101 | "down", 102 | "own", 103 | "just", 104 | "because", 105 | "good", 106 | "each", 107 | "those", 108 | "feel", 109 | "seem", 110 | "how", 111 | "high", 112 | "too", 113 | "place", 114 | "little", 115 | "world", 116 | "very", 117 | "still", 118 | "nation", 119 | "hand", 120 | "old", 121 | "life", 122 | "tell", 123 | "write", 124 | "become", 125 | "here", 126 | "show", 127 | "house", 128 | "both", 129 | "between", 130 | "need", 131 | "mean", 132 | "call", 133 | "develop", 134 | "under", 135 | "last", 136 | "right", 137 | "move", 138 | "thing", 139 | "general", 140 | "school", 141 | "never", 142 | "same", 143 | "another", 144 | "begin", 145 | "while", 146 | "number", 147 | "part", 148 | "turn", 149 | "real", 150 | "leave", 151 | "might", 152 | "want", 153 | "point", 154 | "form", 155 | "off", 156 | "child", 157 | "few", 158 | "small", 159 | "since", 160 | "against", 161 | "ask", 162 | "late", 163 | "home", 164 | "interest", 165 | "large", 166 | "person", 167 | "end", 168 | "open", 169 | "public", 170 | "follow", 171 | "during", 172 | "present", 173 | "without", 174 | "again", 175 | "hold", 176 | "govern", 177 | "around", 178 | "possible", 179 | "head", 180 | "consider", 181 | "word", 182 | "program", 183 | "problem", 184 | "however", 185 | "lead", 186 | "system", 187 | "set", 188 | "order", 189 | "eye", 190 | "plan", 191 | "run", 192 | "keep", 193 | "face", 194 | "fact", 195 | "group", 196 | "play", 197 | "stand", 198 | "increase", 199 | "early", 200 | "course", 201 | "change", 202 | "help", 203 | "line" 204 | ] 205 | } -------------------------------------------------------------------------------- /resources/js/controller.js: -------------------------------------------------------------------------------- 1 | import * as model from "./model.js"; 2 | import Modal from "./views/Modal.js"; 3 | import Stats from "./views/Stats.js"; 4 | import Config from "./views/Config.js"; 5 | import Input from "./views/Input.js"; 6 | import WordsWrapper from "./views/WordsWrapper.js"; 7 | import Caret from "./views/Caret.js"; 8 | import Counter from "./views/Counter.js"; 9 | import Result from "./views/Result.js"; 10 | import Chart from "./views/Chart.js"; 11 | import History from "./views/History.js"; 12 | import Restart from "./views/Restart.js"; 13 | 14 | async function init() { 15 | Config.selectActiveMode(model.state.config.mode); 16 | 17 | Config.selectActiveType(model.state.config.type); 18 | 19 | await model.loadText(); 20 | 21 | WordsWrapper.renderText(model.state.words); 22 | 23 | Caret.moveCaret([""]); 24 | 25 | setCounter(); 26 | } 27 | init(); 28 | 29 | Config.addHandlerConfig(async (config) => { 30 | model.updateConfig(config); 31 | 32 | await model.loadText(); 33 | 34 | WordsWrapper.renderText(model.state.words); 35 | 36 | setCounter(); 37 | }); 38 | 39 | Input.addHandlerInput(function (inputArr, inputType) { 40 | if (!model.state.isTestStarted && inputArr[0]) { 41 | model.startTest(); 42 | 43 | Caret.stopAnimation(); 44 | 45 | Config.hideConfig(); 46 | 47 | if (model.state.config.mode === "time") { 48 | Counter.addHandlerTimer(model.state.config.type, () => { 49 | WordsWrapper.removeUnusedLetters(Input.inputArr); 50 | endTestAndDisplayResult(); 51 | }); 52 | } 53 | } 54 | 55 | WordsWrapper.validateText(inputArr, inputType); 56 | 57 | Caret.moveCaret(inputArr); 58 | 59 | if (model.state.config.mode === "time") return; 60 | 61 | Counter.setCounter(model.state.words.length, inputArr.length - 1); 62 | 63 | if (inputArr.length < model.state.words.length) return; 64 | 65 | if (isTestComplete(inputArr)) { 66 | endTestAndDisplayResult(); 67 | } 68 | }); 69 | 70 | Stats.addHandlerStatBtn(() => { 71 | Modal.showModal(); 72 | 73 | Stats.renderStats(model.state.stats, model.state.history); 74 | }); 75 | 76 | History.addHistoryOptionHandler((btn) => { 77 | model.updateConfig({ historyOption: btn }); 78 | 79 | History.DisplayHistoryOption( 80 | model.state.history, 81 | model.state.testResult.words 82 | ); 83 | }); 84 | 85 | Restart.addHandlerRestart(async (option) => { 86 | option !== "restart" && (await model.loadText()); 87 | 88 | Restart.hideRestartBtn(); 89 | 90 | WordsWrapper.renderText(model.state.words); 91 | 92 | model.resetTest(); 93 | 94 | Input.clearInput(); 95 | 96 | Input.focusInput(); 97 | 98 | Config.showConfig(); 99 | 100 | Caret.startAnimation(); 101 | 102 | Caret.moveCaret([""]); 103 | 104 | Counter.resetCounter(); 105 | 106 | WordsWrapper.resetErrorCount(); 107 | 108 | setCounter(); 109 | 110 | if (!model.state.isTestStarted) { 111 | Input.enableInput(); 112 | 113 | Result.hideResult(); 114 | 115 | Caret.showCaret(); 116 | } 117 | }); 118 | 119 | function endTestAndDisplayResult() { 120 | Input.disableInput(); 121 | 122 | model.endTest(); 123 | 124 | Caret.hideCaret(); 125 | 126 | model.updateResult(WordsWrapper.getResults()); 127 | 128 | Result.showResult(model.state.testResult); 129 | 130 | Restart.showRestartBtn(); 131 | 132 | History.selectActiveOption(model.state.config.historyOption); 133 | 134 | History.DisplayHistoryOption( 135 | model.state.history, 136 | model.state.testResult.words 137 | ); 138 | 139 | History.addHistoryDetailHandler((id) => { 140 | const item = model.state.history.find((val) => val.id == id); 141 | 142 | if (model.state.testResult === item) return; 143 | 144 | model.state.testResult = item; 145 | 146 | Result.showResult(item); 147 | 148 | Chart.displayChart(item); 149 | }); 150 | 151 | Chart.displayChart(model.state.testResult); 152 | 153 | console.log(model.state.testResult); 154 | 155 | return; 156 | } 157 | 158 | function setCounter() { 159 | model.state.config.mode === "time" && 160 | Counter.setTimeCounter(model.state.config.type); 161 | model.state.config.mode !== "time" && 162 | Counter.setCounter(model.state.words.length); 163 | } 164 | 165 | function isTestComplete(inputArr) { 166 | const wordCount = inputArr.length - 1; 167 | const wordsLength = model.state.words.length; 168 | const inputLastWord = inputArr.slice(-1)[0]; 169 | const wordsLastWord = model.state.words.slice(-1)[0]; 170 | 171 | return ( 172 | wordCount === wordsLength || 173 | (wordCount === wordsLength - 1 && inputLastWord === wordsLastWord) 174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /resources/js/views/WordsWrapper.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class WordsWrapper extends View { 4 | _parentElement = document.querySelector(".words-wrapper"); 5 | _errorCount = 0; 6 | 7 | renderText(words) { 8 | this._parentElement.innerHTML = ""; 9 | 10 | // Create word and letter elements for each word and letters 11 | words.forEach((word) => { 12 | var letterEl = ""; 13 | 14 | for (let i = 0; i < word.length; i++) { 15 | letterEl += `
${word[i]}
`; 16 | } 17 | 18 | const wordEl = `
${letterEl}
`; 19 | 20 | this._parentElement.insertAdjacentHTML("beforeend", wordEl); 21 | }); 22 | } 23 | 24 | validateText(inputArr, inputType) { 25 | const currWord = this._getWordOfType(inputArr.length); 26 | 27 | const currInputWordArr = inputArr[inputArr.length - 1].split(""); 28 | 29 | currWord && currWord.classList.remove("incorrect"); 30 | 31 | currWord && this._clearClass(currWord.childNodes, ["correct", "incorrect"]); 32 | 33 | currWord && this._removeExtraLetters(currWord); 34 | 35 | currInputWordArr.forEach((letter, index) => { 36 | const letterEl = currWord.childNodes[index]; 37 | 38 | if (!letterEl) { 39 | const extraLetter = `
${letter}
`; 40 | 41 | return (currWord.innerHTML += extraLetter); 42 | } 43 | 44 | // Validate letter 45 | if (letterEl.textContent === letter) letterEl.classList.add("correct"); 46 | else letterEl.classList.add("incorrect"); 47 | }); 48 | 49 | // Validate previous word 50 | if (!currInputWordArr[0]) { 51 | const prevWord = this._getWordOfType(inputArr.length - 1); 52 | 53 | if (prevWord?.querySelector(":not(.correct)")) { 54 | prevWord.classList.add("incorrect"); 55 | 56 | this._errorCount++; 57 | } 58 | return; 59 | } 60 | 61 | // Validate letter for error count 62 | if (inputType !== "insertText") return; 63 | 64 | const currLetter = currWord?.childNodes[inputArr.slice(-1)[0].length - 1]; 65 | 66 | if (!currLetter.classList.contains("correct")) this._errorCount++; 67 | } 68 | 69 | getResults() { 70 | const results = { 71 | wordCount: this._wordCount.length, 72 | letterCount: this._letterCount.length, 73 | correctLetters: this._correctCharacters.length, 74 | incorrectLetters: this._incorrectCharacters.length, 75 | missedLetters: this._missedCharacters.length, 76 | extraLetters: this._extraLetters.length, 77 | errorCount: this._errorCount, 78 | words: this._parentElement.innerHTML, 79 | }; 80 | 81 | return results; 82 | } 83 | 84 | removeUnusedLetters(inputArr) { 85 | const wordsEl = document.querySelectorAll(".word"); 86 | 87 | const isWordEmpty = inputArr[inputArr.length - 1] ? 1 : 2; 88 | 89 | const currWord = wordsEl[inputArr.length - isWordEmpty]; 90 | 91 | this._removeNextElementSibling(currWord); 92 | 93 | const currLetterIndex = inputArr.slice(-1)[0].length - 1; 94 | 95 | const currLetter = currWord?.childNodes[currLetterIndex]; 96 | 97 | if (!currLetter) return; 98 | 99 | this._removeNextElementSibling(currLetter); 100 | } 101 | 102 | resetErrorCount() { 103 | this._errorCount = 0; 104 | } 105 | 106 | get _correctCharacters() { 107 | return this._parentElement.querySelectorAll(".letter.correct"); 108 | } 109 | 110 | get _incorrectCharacters() { 111 | return this._parentElement.querySelectorAll(".letter.incorrect"); 112 | } 113 | 114 | get _missedCharacters() { 115 | return this._parentElement.querySelectorAll("[class='letter']"); 116 | } 117 | 118 | get _wordCount() { 119 | return this._parentElement.querySelectorAll(".word"); 120 | } 121 | 122 | get _letterCount() { 123 | return this._parentElement.querySelectorAll(".letter"); 124 | } 125 | 126 | get _extraLetters() { 127 | return this._parentElement.querySelectorAll(".letter.extra"); 128 | } 129 | 130 | _removeNextElementSibling(startEl) { 131 | const elSibling = startEl.nextSibling; 132 | 133 | if (!elSibling) return; 134 | 135 | this._removeNextElementSibling(elSibling); 136 | 137 | elSibling.remove(); 138 | } 139 | 140 | _removeExtraLetters(wordEl) { 141 | const extraLetters = wordEl.querySelectorAll(".extra"); 142 | 143 | extraLetters.forEach((letter) => { 144 | letter.remove(); 145 | }); 146 | } 147 | } 148 | 149 | export default new WordsWrapper(); 150 | -------------------------------------------------------------------------------- /resources/images/sprite.svg: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /resources/images/symbol-defs.svg: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /resources/js/views/Stats.js: -------------------------------------------------------------------------------- 1 | import View from "./View.js"; 2 | 3 | class Stats extends View { 4 | _parentElement = document.querySelector("#stats-btn"); 5 | _modalContent = document.querySelector(".modal-content"); 6 | 7 | addHandlerStatBtn(handler) { 8 | this._parentElement.addEventListener("click", () => { 9 | handler(); 10 | }); 11 | } 12 | 13 | renderStats(stats, historyArr) { 14 | this._modalContent.innerHTML = ""; 15 | 16 | const wpmList = historyArr.map((d) => d.wpm); 17 | const accList = historyArr.map((d) => d.acc); 18 | 19 | const element = ` 20 |
21 |
22 | 23 | 24 | 25 | Stats 26 |
27 |
28 |
29 |
Tests started
30 |
${ 31 | stats.testStarted 32 | }
33 |
34 |
35 |
Tests completed
36 |
${ 37 | stats.testCompleted 38 | }
39 |
40 |
41 |
time typing
42 |
${this._formatTime( 43 | stats.timeTyping 44 | )}
45 |
46 |
47 |
highest wpm
48 |
${Math.round( 49 | this._highest(wpmList) 50 | )}
51 |
52 |
53 |
average wpm
54 |
${Math.round( 55 | this._average(wpmList) 56 | )}
57 |
58 |
59 |
average wpm (last 10 tests)
60 |
${Math.round( 61 | this._averageLastN(wpmList, 10) 62 | )}
63 |
64 |
65 |
highest accuracy
66 |
${Math.round( 67 | this._highest(accList) 68 | )}%
69 |
70 |
71 |
average accuracy
72 |
${Math.round( 73 | this._average(accList) 74 | )}%
75 |
76 |
77 |
average accuracy (last 10 tests)
78 |
${Math.round( 79 | this._averageLastN(accList, 10) 80 | )}%
81 |
82 |
83 |
Toggle History 84 | 85 | 86 | 87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
wpmaccuracy %modetypecharsdate
99 |
100 |
101 | `; 102 | 103 | this._modalContent.insertAdjacentHTML("beforeend", element); 104 | 105 | this._renderHistories(historyArr); 106 | 107 | const viewHistoryBtn = document.querySelector(".view-history-btn"); 108 | const recentContainer = document.querySelector(".recent-container"); 109 | const statsContainer = document.querySelector(".stats"); 110 | 111 | viewHistoryBtn.addEventListener("click", () => { 112 | recentContainer.classList.toggle("active"); 113 | statsContainer.classList.toggle("expand"); 114 | }); 115 | } 116 | 117 | _average(arr) { 118 | const sum = arr.reduce((a, b) => a + b, 0); 119 | return sum / arr.length; 120 | } 121 | 122 | _highest(arr) { 123 | return Math.max(...arr); 124 | } 125 | 126 | _averageLastN(arr, n) { 127 | const lastN = arr.slice(-n); 128 | return this._average(lastN); 129 | } 130 | 131 | _formatTime(milliseconds) { 132 | const totalSeconds = Math.floor(milliseconds / 1000); 133 | const hours = Math.floor(totalSeconds / 3600); 134 | const minutes = Math.floor((totalSeconds % 3600) / 60); 135 | const seconds = totalSeconds % 60; 136 | 137 | const formattedTime = `${String(hours).padStart(2, "0")}:${String( 138 | minutes 139 | ).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; 140 | return formattedTime; 141 | } 142 | 143 | _formatDate(milliseconds) { 144 | const date = new Date(milliseconds); 145 | 146 | const options = { 147 | year: "numeric", 148 | month: "short", 149 | day: "numeric", 150 | hour: "2-digit", 151 | minute: "2-digit", 152 | }; 153 | const formattedDate = date.toLocaleDateString("en-US", options); 154 | 155 | return formattedDate; 156 | } 157 | 158 | _renderHistories(histories) { 159 | const recentContainerTable = document.querySelector(".recent-table"); 160 | 161 | histories.forEach((history) => { 162 | const string = ` 163 | 164 | ${history.wpm.toFixed(2)} 165 | ${history.acc.toFixed(2)} % 166 | ${history.mode} 167 | ${history.type} 168 | ${history.correctLetters}/${history.incorrectLetters}/${ 169 | history.extraLetters}/${history.missedLetters} 170 | ${this._formatDate(history.timeElapse.timeStopped)} 171 | 172 | `; 173 | 174 | recentContainerTable.insertAdjacentHTML("beforeend", string); 175 | }); 176 | } 177 | } 178 | 179 | export default new Stats(); 180 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Rocket type 12 | 13 | 14 | 15 | 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 |
words
48 |
49 |
50 | 51 | 52 | 53 |
time
54 |
55 |
56 | 57 | 58 | 59 |
quote
60 |
61 |
62 |
63 |
64 | 65 |
10
66 |
25
67 |
50
68 |
100
69 |
70 |
71 |
15s
72 |
30s
73 |
60s
74 |
120s
75 |
76 |
77 |
all
78 |
short
79 |
medium
80 |
long
81 |
82 |
83 |
84 |
85 | 88 | 103 |
104 | 105 |
106 |
107 |
108 | 109 | 239 |
240 |
241 | 247 | 253 | 259 |
260 |
261 | 262 | 263 | 378 | 379 | 380 | 381 | 382 | -------------------------------------------------------------------------------- /resources/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300;400;500;600&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;500;600&display=swap"); 3 | 4 | :root { 5 | --primary-color: #3cd47e; 6 | --primary-color: #2da762; 7 | --secondary-color: #646669; 8 | --tertiary-color: #0b0b13; 9 | --background-color: #191b1f; 10 | --background-color: #0e1012; 11 | --background-color: #0f0f0f; 12 | --background-color: #16161d; 13 | --background-color-secondary: #050507; 14 | --background-color-secondary: #030304; 15 | --background-color-secondary: rgba(5, 5, 7, 0.9); 16 | --background-color-secondary: rgba(5, 5, 7, 0.7); 17 | 18 | --background-shadow: rgba(0, 0, 0, 0.35) 0px 3px 3px; 19 | --background-shadow: rgba(0, 0, 0, 0.35) 0px 5px 10px; 20 | 21 | --primary-font: "Roboto Mono", monospace; 22 | --text-color: #d1d0c5; 23 | --sub-color: #646669; 24 | --error-color: #ca4754; 25 | --error-extra-color: #7e2a33; 26 | } 27 | 28 | ::-webkit-scrollbar { 29 | width: 0.8rem; 30 | width: 1.2rem; 31 | background-color: transparent; 32 | } 33 | 34 | ::-webkit-scrollbar-corner { 35 | background: transparent; 36 | } 37 | 38 | ::-webkit-scrollbar-thumb { 39 | border-radius: 2rem; 40 | background-color: var(--primary-color); 41 | border: 1.7px solid #0b0a0e; 42 | } 43 | 44 | ::-webkit-scrollbar-track { 45 | background-color: var(--background-color); 46 | border-radius: 2rem; 47 | margin-top: 1rem; 48 | border: 1.7px solid var(--background-color-secondary); 49 | } 50 | 51 | *, 52 | *::before, 53 | *::after { 54 | margin: 0; 55 | padding: 0; 56 | box-sizing: border-box; 57 | } 58 | 59 | html { 60 | font-size: 62.5%; 61 | } 62 | 63 | body { 64 | font-family: "Roboto Mono", monospace; 65 | color: var(--secondary-color); 66 | background-color: var(--background-color); 67 | max-width: 1100px; 68 | max-width: 1000px; 69 | margin: 0 auto; 70 | } 71 | 72 | a, 73 | a:link, 74 | a:visited, 75 | a:active { 76 | text-decoration: none; 77 | color: inherit; 78 | font-family: inherit; 79 | } 80 | 81 | /****************************************/ 82 | /************** Navigation **************/ 83 | /****************************************/ 84 | 85 | .navigation { 86 | display: flex; 87 | align-items: center; 88 | font-size: 1.6rem; 89 | background: var(--background-color-secondary); 90 | padding: 2rem; 91 | margin: 2rem; 92 | border-radius: 2rem; 93 | } 94 | 95 | .logo { 96 | margin-right: auto; 97 | display: flex; 98 | align-items: center; 99 | gap: 1.5rem; 100 | } 101 | 102 | .logo-img { 103 | opacity: 0.4; 104 | width: 4rem; 105 | padding-left: 1rem; 106 | } 107 | 108 | .logo-text { 109 | font-size: 2.2rem; 110 | font-weight: 500; 111 | font-family: "Lexend Deca", sans-serif; 112 | } 113 | 114 | .nav-links { 115 | list-style: none; 116 | display: flex; 117 | gap: 2rem; 118 | background: var(--background-color-secondary); 119 | border-radius: 1rem; 120 | padding: 1rem 2rem; 121 | } 122 | 123 | .nav-link { 124 | display: flex; 125 | gap: 1.4rem; 126 | align-items: center; 127 | position: relative; 128 | padding: 1rem 0.5rem; 129 | font-size: 1.6rem; 130 | font-weight: 500; 131 | color: var(--secondary-color); 132 | cursor: pointer; 133 | transition: color 0.2s ease-in-out; 134 | } 135 | 136 | .nav-link:hover { 137 | color: var(--text-color); 138 | } 139 | 140 | .nav-link:hover > .nav-icon { 141 | fill: var(--text-color); 142 | } 143 | 144 | .nav-icon { 145 | fill: var(--secondary-color); 146 | width: 2rem; 147 | height: 2rem; 148 | transition: fill 0.2s ease-in-out; 149 | } 150 | 151 | /****************************************/ 152 | /************** Main Header *************/ 153 | /****************************************/ 154 | 155 | .main { 156 | padding: 0 2rem; 157 | } 158 | 159 | /*****************/ 160 | /*** Dashboard ***/ 161 | /*****************/ 162 | 163 | .dashboard { 164 | display: flex; 165 | flex-direction: column; 166 | justify-content: center; 167 | align-items: center; 168 | position: relative; 169 | background-color: var(--background-color-secondary); 170 | box-shadow: var(--background-shadow); 171 | width: 70%; 172 | margin: 0 auto; 173 | border-radius: 2rem; 174 | height: 17rem; 175 | } 176 | 177 | /*** Config Container ***/ 178 | 179 | .config-container { 180 | display: grid; 181 | grid-template-columns: 1fr auto 1fr; 182 | justify-items: center; 183 | align-items: center; 184 | gap: 1rem; 185 | } 186 | 187 | .config-container.hidden { 188 | display: none; 189 | } 190 | 191 | /*** Counter ***/ 192 | 193 | .counter { 194 | position: relative; 195 | left: 0; 196 | text-align: center; 197 | font-size: 9rem; 198 | color: var(--primary-color); 199 | font-weight: 500; 200 | letter-spacing: 0.5rem; 201 | transition: all 0.2s ease; 202 | } 203 | 204 | .counter.expand { 205 | font-size: 10rem; 206 | font-weight: 500; 207 | /* transform: translateX(50%); */ 208 | /* left: 50%; */ 209 | left: calc(50% + 1rem); 210 | } 211 | 212 | .divider { 213 | content: ""; 214 | display: table; 215 | width: 5px; 216 | height: 80px; 217 | border-radius: 10px; 218 | background-color: var(--background-color); 219 | } 220 | 221 | .divider.hide { 222 | transform: scaleX(0); 223 | opacity: 0; 224 | } 225 | 226 | /*** Configs ***/ 227 | 228 | .configs { 229 | display: flex; 230 | flex-direction: column; 231 | gap: 1rem; 232 | transform-origin: right; 233 | transition: all 0.2s ease; 234 | } 235 | 236 | .configs.hide { 237 | transform: scaleX(0); 238 | opacity: 0; 239 | } 240 | 241 | .mode-container, 242 | .type-container { 243 | display: flex; 244 | position: relative; 245 | align-items: center; 246 | justify-content: center; 247 | color: var(--secondary-color); 248 | gap: 1rem; 249 | } 250 | 251 | .mode-button, 252 | .type-button { 253 | padding: 0.5rem 1.05rem; 254 | font-weight: 500; 255 | cursor: pointer; 256 | transition: all 0.2s ease; 257 | } 258 | 259 | .mode-button:hover, 260 | .type-button:hover { 261 | color: #d1d0c5; 262 | } 263 | 264 | .mode-button:active, 265 | .type-button:active { 266 | color: var(--secondary-color); 267 | } 268 | 269 | .mode-button.active, 270 | .type-button.active { 271 | color: var(--primary-color); 272 | font-weight: 600; 273 | } 274 | 275 | .mode-button.active .mode-icon { 276 | fill: var(--primary-color); 277 | } 278 | 279 | .mode-button.active:hover .mode-icon { 280 | fill: var(--primary-color); 281 | } 282 | 283 | .mode-button { 284 | display: flex; 285 | align-items: center; 286 | justify-content: center; 287 | gap: 1.1rem; 288 | font-size: 1.55rem; 289 | line-height: 1.1rem; 290 | } 291 | 292 | .mode-button:hover > .mode-icon { 293 | fill: #d1d0c5; 294 | } 295 | 296 | .mode-button:active > .mode-icon { 297 | fill: var(--secondary-color); 298 | } 299 | 300 | .mode-icon { 301 | fill: var(--secondary-color); 302 | width: 1.3rem; 303 | height: 1.3rem; 304 | transition: fill 0.1s linear; 305 | } 306 | 307 | .type-button { 308 | font-size: 1.4rem; 309 | } 310 | 311 | .type { 312 | align-items: center; 313 | display: none; 314 | gap: 1rem; 315 | } 316 | 317 | .type.active { 318 | display: flex; 319 | } 320 | 321 | /*** Result container ***/ 322 | 323 | .result-container { 324 | display: grid; 325 | grid-template-columns: 1fr 1fr 1fr; 326 | justify-content: space-between; 327 | align-items: center; 328 | width: 100%; 329 | height: 100%; 330 | } 331 | 332 | .result-container.hidden { 333 | display: none; 334 | } 335 | 336 | .result-item { 337 | display: flex; 338 | flex-direction: column; 339 | align-items: center; 340 | font-size: 4rem; 341 | color: var(--primary-color); 342 | position: relative; 343 | align-items: center; 344 | } 345 | 346 | .result-label { 347 | font-size: 1.5rem; 348 | color: var(--secondary-color); 349 | } 350 | 351 | /*****************/ 352 | /** Typing Area **/ 353 | /*****************/ 354 | 355 | .words-input { 356 | position: absolute; 357 | top: 0; 358 | left: 0; 359 | z-index: -1; 360 | opacity: 0; 361 | user-select: none; 362 | } 363 | 364 | .words-container { 365 | position: relative; 366 | margin-top: 2.5rem; 367 | background-color: var(--background-color-secondary); 368 | box-shadow: var(--background-shadow); 369 | border-radius: 2rem; 370 | /* overflow: hidden; */ 371 | } 372 | 373 | .words-wrapper { 374 | display: flex; 375 | flex-wrap: wrap; 376 | font-size: 2.3rem; 377 | padding: 2.5rem 3rem; 378 | color: var(--secondary-color); 379 | cursor: default; 380 | overflow-wrap: break-word; 381 | position: relative; 382 | padding: 2rem; 383 | padding-bottom: 0.1rem; 384 | max-height: 17.6rem; 385 | overflow: hidden; 386 | transition: max-height 0.2s ease-in-out; 387 | /* transition: transform .2s ease; */ 388 | } 389 | 390 | .words-wrapper::-webkit-scrollbar { 391 | width: 0; 392 | height: 0; 393 | background-color: transparent; 394 | } 395 | 396 | .words-wrapper.hidden { 397 | display: none; 398 | } 399 | 400 | .caret { 401 | position: absolute; 402 | top: 3.7rem; 403 | left: 3.9rem; 404 | width: 0.28rem; 405 | height: 2.8rem; 406 | border-radius: 2px; 407 | background-color: var(--primary-color); 408 | transition: all 0.1s; 409 | margin-top: 0.2rem; 410 | z-index: 10; 411 | } 412 | 413 | .caret.hide { 414 | opacity: 0; 415 | } 416 | 417 | .caret.animate { 418 | animation: blink 1s infinite; 419 | } 420 | 421 | .word { 422 | margin: 0.5rem; 423 | margin: 0 0.5rem; 424 | line-height: 3rem; 425 | padding-bottom: 2rem; 426 | padding-top: 0; 427 | transition: all 0.1s ease; 428 | } 429 | 430 | .word::after { 431 | content: ""; 432 | display: block; 433 | position: relative; 434 | top: -0.3rem; 435 | height: 2px; 436 | width: 100%; 437 | border-radius: 2px; 438 | transform: scaleX(0); 439 | transition: transform 0.4s ease; 440 | transform-origin: left; 441 | } 442 | 443 | .word.incorrect::after { 444 | background-color: var(--error-color); 445 | transform: scaleX(1); 446 | } 447 | 448 | .word .letter { 449 | display: inline-block; 450 | transition: all 0.1s ease; 451 | } 452 | 453 | .word .letter.correct { 454 | color: var(--text-color); 455 | } 456 | 457 | .word .letter.incorrect { 458 | color: var(--error-color); 459 | } 460 | 461 | .word .letter.extra { 462 | color: var(--error-extra-color); 463 | } 464 | 465 | /********************/ 466 | /** Result Wrapper **/ 467 | /********************/ 468 | .result-wrapper { 469 | padding: 2.5rem; 470 | display: grid; 471 | grid-template-columns: 35% auto; 472 | /* grid-template-columns: 30rem auto; */ 473 | /* grid-template-columns: 1fr 2fr; */ 474 | } 475 | 476 | .result-wrapper.hidden { 477 | display: none; 478 | } 479 | 480 | /** Doughnut **/ 481 | .character-details-container { 482 | display: flex; 483 | flex-direction: column; 484 | } 485 | 486 | .donut-container { 487 | width: 19rem; 488 | align-self: center; 489 | transform: translateY(-1rem); 490 | } 491 | 492 | .donut-details { 493 | display: grid; 494 | grid-template-columns: 1fr 1fr; 495 | gap: 2rem 2.5rem; 496 | } 497 | 498 | .donut-detail { 499 | padding-bottom: 2rem; 500 | padding-left: 2rem; 501 | cursor: default; 502 | } 503 | 504 | .donut-detail_title { 505 | font-size: 1.2rem; 506 | margin-bottom: 0.5rem; 507 | font-weight: 500; 508 | } 509 | 510 | .donut-detail_value { 511 | font-size: 1.9rem; 512 | color: var(--text-color); 513 | display: flex; 514 | align-items: center; 515 | } 516 | 517 | .donut-detail_value::before { 518 | content: ""; 519 | display: inline-block; 520 | height: 1.9rem; 521 | width: 4px; 522 | border-radius: 1rem; 523 | margin-right: 1rem; 524 | } 525 | 526 | .detail-correct::before { 527 | background-color: var(--primary-color); 528 | } 529 | 530 | .detail-errors::before { 531 | background-color: var(--error-color); 532 | background-color: #ff6384; 533 | } 534 | 535 | .detail-extra::before { 536 | background-color: var(--error-extra-color); 537 | background-color: #36a2eb; 538 | } 539 | 540 | .detail-missed::before { 541 | background-color: var(--sub-color); 542 | background-color: #ffcd56; 543 | } 544 | 545 | /** Flex Container **/ 546 | 547 | .right-container { 548 | margin-left: 2.5rem; 549 | } 550 | 551 | .test-config-container { 552 | display: grid; 553 | grid-template-columns: repeat(4, 1fr); 554 | justify-content: space-between; 555 | gap: 2rem; 556 | margin-bottom: 2rem; 557 | } 558 | 559 | .test-config { 560 | text-align: center; 561 | } 562 | 563 | .test-config_title { 564 | font-size: 1.4rem; 565 | } 566 | 567 | .test-config_value { 568 | font-size: 1.6rem; 569 | font-weight: 800; 570 | color: var(--text-color); 571 | } 572 | 573 | /** History Wrapper **/ 574 | 575 | .history-options-container { 576 | display: grid; 577 | grid-template-columns: repeat(3, 1fr); 578 | gap: 1.5rem; 579 | padding: 0.5rem; 580 | border-radius: 1rem; 581 | border: 1.5px solid var(--background-color); 582 | } 583 | 584 | .history-option { 585 | font-size: 1.4rem; 586 | padding: 1rem 2rem; 587 | border-radius: 0.7rem; 588 | text-align: center; 589 | color: var(--text-color); 590 | cursor: pointer; 591 | transition: all 0.2s ease; 592 | } 593 | 594 | .history-option.active { 595 | background-color: var(--background-color); 596 | } 597 | 598 | .history-option:hover:not(.history-option.active) { 599 | background-color: rgba(22, 22, 29, 0.4); 600 | } 601 | 602 | .history-container { 603 | display: flex; 604 | flex-direction: column; 605 | justify-content: flex-start; 606 | gap: 0rem; 607 | padding-top: 0.5rem; 608 | height: 24rem; 609 | overflow: auto; 610 | } 611 | 612 | .history-detail { 613 | display: grid; 614 | grid-template-columns: repeat(4, 1fr); 615 | justify-items: center; 616 | padding: 1.2rem 0; 617 | margin-right: 0.5rem; 618 | border-radius: 0.7rem; 619 | transition: all 0.2s ease; 620 | } 621 | 622 | .history-detail:hover { 623 | background: var(--background-color); 624 | cursor: pointer; 625 | } 626 | 627 | .history-item { 628 | display: flex; 629 | flex-direction: column; 630 | justify-content: center; 631 | } 632 | 633 | .history-item_title { 634 | font-size: 1.2rem; 635 | } 636 | 637 | .history-item_value { 638 | font-size: 1.4rem; 639 | color: var(--text-color); 640 | text-align: center; 641 | } 642 | 643 | .history-time { 644 | font-size: 1.3rem; 645 | width: 2rem; 646 | text-align: center; 647 | font-weight: 500; 648 | } 649 | 650 | .input-history { 651 | display: flex; 652 | flex-wrap: wrap; 653 | align-content: flex-start; 654 | font-size: 1.5rem; 655 | padding-left: 3rem; 656 | height: 22rem; 657 | overflow: auto; 658 | } 659 | 660 | .recent-container { 661 | background-color: var(--background-color-secondary); 662 | margin-top: 1rem; 663 | padding: 2rem; 664 | padding: 1rem; 665 | border-radius: 1rem; 666 | 667 | height: 30rem; 668 | height: 35rem; 669 | overflow-y: auto; 670 | height: 0; 671 | opacity: 0; 672 | transition: all 0.3s ease; 673 | } 674 | 675 | .recent-container.active { 676 | height: 35rem; 677 | opacity: 1; 678 | } 679 | 680 | .recent-table { 681 | width: 100%; 682 | margin: 0 auto; 683 | border-collapse: collapse; 684 | height: 3rem; 685 | } 686 | 687 | .recent-table th, 688 | .recent-table td { 689 | text-align: center; 690 | text-align: start; 691 | padding: 1rem; 692 | border-bottom: 2px solid var(--background-color); 693 | width: 20rem; 694 | } 695 | 696 | .recent-table th { 697 | font-size: 1.2rem; 698 | } 699 | 700 | .recent-table tr td:last-child { 701 | width: 30rem; 702 | width: 40rem; 703 | } 704 | 705 | .recent-table td { 706 | font-size: 1.6rem; 707 | font-size: 1.5rem; 708 | color: var(--text-color); 709 | 710 | /* vertical-align: bottom; */ 711 | padding: 2.5rem 1rem; 712 | /* padding: 2rem 1rem; */ 713 | } 714 | 715 | /************/ 716 | /** Loader **/ 717 | /************/ 718 | .custom-loader { 719 | width: 50px; 720 | height: 50px; 721 | border-radius: 50%; 722 | border: 8px solid; 723 | border-color: var(--primary-color) #0000; 724 | animation: s1 1s infinite; 725 | position: absolute; 726 | top: 50%; 727 | left: 50%; 728 | /* transform: translate(-50%, -50%); */ 729 | } 730 | 731 | /*************/ 732 | /** Restart **/ 733 | /*************/ 734 | .restart-container { 735 | display: flex; 736 | justify-content: center; 737 | gap: 2rem; 738 | margin-top: 2rem; 739 | } 740 | 741 | .restart-btn { 742 | display: flex; 743 | align-items: center; 744 | justify-content: center; 745 | color: var(--secondary-color); 746 | border: none; 747 | cursor: pointer; 748 | font-family: inherit; 749 | padding: 1.2rem 2rem; 750 | border-radius: 1rem; 751 | font-size: 1.7rem; 752 | gap: 1.5rem; 753 | background-color: var(--background-color-secondary); 754 | box-shadow: var(--background-shadow); 755 | transition: color 0.2s ease-in-out; 756 | } 757 | 758 | .restart-btn.hidden { 759 | display: none; 760 | } 761 | 762 | .restart-btn:hover { 763 | color: var(--text-color); 764 | } 765 | 766 | .restart-btn:hover svg { 767 | fill: var(--text-color); 768 | } 769 | 770 | .restart-btn:focus { 771 | transition: none; 772 | color: var(--text-color); 773 | } 774 | 775 | .restart-btn:focus svg { 776 | transition: none; 777 | fill: var(--text-color); 778 | } 779 | 780 | .restart:hover .restart-icon { 781 | transform: rotate(0.5turn) scale(1.1); 782 | } 783 | 784 | .next-test:hover .restart-icon { 785 | transform: translateX(0.5rem); 786 | } 787 | 788 | .restart-icon { 789 | width: 2rem; 790 | height: 2rem; 791 | fill: var(--secondary-color); 792 | transition: fill 0.2s ease-in-out, transform 0.2s ease-in-out; 793 | } 794 | 795 | /***********/ 796 | /** Modal **/ 797 | /***********/ 798 | .modal { 799 | position: fixed; 800 | top: 50%; 801 | left: 50%; 802 | transform: translate(-50%, -50%); 803 | /* max-width: 60rem; */ 804 | background-color: #f3f3f3; 805 | /* padding: 5rem 6rem; */ 806 | /* box-shadow: 0 4rem 6rem rgba(0, 0, 0, 0.3); */ 807 | /* box-shadow: var(--background-shadow); */ 808 | z-index: 1000; 809 | transition: all 0.5s; 810 | 811 | transition: all 0.1s; 812 | background-color: var(--background-color); 813 | border-radius: 1rem; 814 | /* padding: 4rem; */ 815 | width: 100rem; 816 | } 817 | 818 | .modal-content { 819 | padding: 3rem; 820 | /* padding-bottom: 2.5rem; */ 821 | } 822 | 823 | .overlay { 824 | position: fixed; 825 | top: 0; 826 | left: 0; 827 | width: 100%; 828 | height: 100%; 829 | background-color: rgba(0, 0, 0, 0.5); 830 | backdrop-filter: blur(4px); 831 | z-index: 100; 832 | transition: all 0.5s; 833 | transition: all 0.1s; 834 | } 835 | 836 | .btn--close-modal { 837 | font-family: inherit; 838 | color: inherit; 839 | position: absolute; 840 | top: 2rem; 841 | right: 3rem; 842 | font-size: 4rem; 843 | line-height: 2.5rem; 844 | cursor: pointer; 845 | border: none; 846 | background: none; 847 | transition: all 0.1s ease-in-out; 848 | } 849 | 850 | .btn--close-modal:hover { 851 | color: var(--text-color); 852 | } 853 | 854 | .modal.hidden, 855 | .overlay.hidden { 856 | visibility: hidden; 857 | opacity: 0; 858 | } 859 | 860 | .stats { 861 | height: 37.5rem; 862 | transition: height 0.3s ease; 863 | } 864 | 865 | .stats.expand { 866 | height: 73.5rem; 867 | } 868 | 869 | .stats-title { 870 | font-size: 2rem; 871 | color: var(--text-color); 872 | margin-bottom: 2rem; 873 | margin-bottom: 3rem; 874 | text-align: center; 875 | } 876 | 877 | .stats-title svg { 878 | fill: var(--text-color); 879 | } 880 | 881 | .stats-content-container { 882 | display: grid; 883 | grid-template-columns: repeat(3, 1fr); 884 | /* grid-template-columns: repeat(2, 1fr); */ 885 | gap: 2rem; 886 | /* gap: 1.5rem; */ 887 | border-radius: 1rem; 888 | } 889 | 890 | .stats-content { 891 | background-color: var(--background-color-secondary); 892 | border-radius: 1rem; 893 | padding: 2rem; 894 | padding: 1.5rem 2rem; 895 | } 896 | 897 | .stats-content__title { 898 | font-size: 1.3rem; 899 | font-weight: 500; 900 | } 901 | 902 | .stats-content__value { 903 | font-size: 1.8rem; 904 | font-size: 2rem; 905 | font-size: 2.2rem; 906 | font-weight: 500; 907 | color: var(--text-color); 908 | } 909 | 910 | .view-history-btn { 911 | display: flex; 912 | justify-content: center; 913 | align-items: center; 914 | gap: 1rem; 915 | /* text-align: center; */ 916 | margin-top: 2rem; 917 | font-size: 1.2rem; 918 | font-size: 1.3rem; 919 | color: var(--text-color); 920 | cursor: pointer; 921 | background-color: var(--background-color-secondary); 922 | padding: 0.5rem 1rem; 923 | border-radius: 0.8rem; 924 | } 925 | 926 | .stats-view-history-btn svg { 927 | fill: var(--text-color); 928 | } 929 | 930 | /***************/ 931 | /** Animation **/ 932 | /***************/ 933 | @keyframes s1 { 934 | to { 935 | transform: rotate(0.5turn); 936 | } 937 | } 938 | 939 | @keyframes blink { 940 | 0%, 941 | 100% { 942 | opacity: 1; 943 | } 944 | 945 | 50% { 946 | opacity: 0; 947 | } 948 | } 949 | -------------------------------------------------------------------------------- /words/quotes/english.json: -------------------------------------------------------------------------------- 1 | { 2 | "quotes": [ 3 | { 4 | "text": "The only way to do great work is to love what you do.", 5 | "author": "Steve Jobs", 6 | "id": 1, 7 | "char": 41, 8 | "length": "short" 9 | }, 10 | { 11 | "text": "In the middle of every difficulty lies opportunity.", 12 | "author": "Albert Einstein", 13 | "id": 2, 14 | "char": 44, 15 | "length": "short" 16 | }, 17 | { 18 | "text": "Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful.", 19 | "author": "Albert Schweitzer", 20 | "id": 3, 21 | "char": 102, 22 | "length": "long" 23 | }, 24 | { 25 | "text": "The best way to predict the future is to create it.", 26 | "author": "Peter Drucker", 27 | "id": 4, 28 | "char": 41, 29 | "length": "short" 30 | }, 31 | { 32 | "text": "Believe you can and you're halfway there.", 33 | "author": "Theodore Roosevelt", 34 | "id": 5, 35 | "char": 35, 36 | "length": "short" 37 | }, 38 | { 39 | "text": "Don't watch the clock; do what it does. Keep going.", 40 | "author": "Sam Levenson", 41 | "id": 6, 42 | "char": 42, 43 | "length": "short" 44 | }, 45 | { 46 | "text": "Your time is limited, don't waste it living someone else's life.", 47 | "author": "Steve Jobs", 48 | "id": 7, 49 | "char": 54, 50 | "length": "short" 51 | }, 52 | { 53 | "text": "I find that the harder I work, the more luck I seem to have.", 54 | "author": "Thomas Jefferson", 55 | "id": 8, 56 | "char": 47, 57 | "length": "short" 58 | }, 59 | { 60 | "text": "Success is not in what you have, but who you are.", 61 | "author": "Bo Bennett", 62 | "id": 9, 63 | "char": 39, 64 | "length": "short" 65 | }, 66 | { 67 | "text": "The only place where success comes before work is in the dictionary.", 68 | "author": "Vidal Sassoon", 69 | "id": 10, 70 | "char": 57, 71 | "length": "medium" 72 | }, 73 | { 74 | "text": "The biggest risk is not taking any risk. In a world that's changing quickly, the only strategy that is guaranteed to fail is not taking risks.", 75 | "author": "Mark Zuckerberg", 76 | "id": 11, 77 | "char": 117, 78 | "length": "long" 79 | }, 80 | { 81 | "text": "Choose a job you love, and you will never have to work a day in your life.", 82 | "author": "Confucius", 83 | "id": 12, 84 | "char": 58, 85 | "length": "medium" 86 | }, 87 | { 88 | "text": "The future belongs to those who believe in the beauty of their dreams.", 89 | "author": "Eleanor Roosevelt", 90 | "id": 13, 91 | "char": 58, 92 | "length": "medium" 93 | }, 94 | { 95 | "text": "You miss 100% of the shots you don't take.", 96 | "author": "Wayne Gretzky", 97 | "id": 14, 98 | "char": 34, 99 | "length": "short" 100 | }, 101 | { 102 | "text": "It does not matter how slowly you go as long as you do not stop.", 103 | "author": "Confucius", 104 | "id": 15, 105 | "char": 50, 106 | "length": "short" 107 | }, 108 | { 109 | "text": "The only limit to our realization of tomorrow will be our doubts of today.", 110 | "author": "Franklin D. Roosevelt", 111 | "id": 16, 112 | "char": 61, 113 | "length": "medium" 114 | }, 115 | { 116 | "text": "The way to get started is to quit talking and begin doing.", 117 | "author": "Walt Disney", 118 | "id": 17, 119 | "char": 47, 120 | "length": "short" 121 | }, 122 | { 123 | "text": "Life is 10% what happens to us and 90% how we react to it.", 124 | "author": "Charles R. Swindoll", 125 | "id": 18, 126 | "char": 45, 127 | "length": "short" 128 | }, 129 | { 130 | "text": "Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work. And the only way to do great work is to love what you do.", 131 | "author": "Steve Jobs", 132 | "id": 19, 133 | "char": 149, 134 | "length": "long" 135 | }, 136 | { 137 | "text": "The best revenge is massive success.", 138 | "author": "Frank Sinatra", 139 | "id": 20, 140 | "char": 31, 141 | "length": "short" 142 | }, 143 | { 144 | "text": "I have not failed. I've just found 10,000 ways that won't work.", 145 | "author": "Thomas Edison", 146 | "id": 21, 147 | "char": 52, 148 | "length": "short" 149 | }, 150 | { 151 | "text": "The only person you are destined to become is the person you decide to be.", 152 | "author": "Ralph Waldo Emerson", 153 | "id": 22, 154 | "char": 60, 155 | "length": "medium" 156 | }, 157 | { 158 | "text": "Opportunities don't happen. You create them.", 159 | "author": "Chris Grosser", 160 | "id": 23, 161 | "char": 39, 162 | "length": "short" 163 | }, 164 | { 165 | "text": "The best way to predict your future is to create it.", 166 | "author": "Peter Drucker", 167 | "id": 24, 168 | "char": 42, 169 | "length": "short" 170 | }, 171 | { 172 | "text": "The only thing standing between you and your goal is the story you keep telling yourself as to why you can't achieve it.", 173 | "author": "Jordan Belfort", 174 | "id": 25, 175 | "char": 98, 176 | "length": "long" 177 | }, 178 | { 179 | "text": "The secret of getting ahead is getting started.", 180 | "author": "Mark Twain", 181 | "id": 26, 182 | "char": 40, 183 | "length": "short" 184 | }, 185 | { 186 | "text": "Don't be afraid to give up the good to go for the great.", 187 | "author": "John D. Rockefeller", 188 | "id": 27, 189 | "char": 44, 190 | "length": "short" 191 | }, 192 | { 193 | "text": "If you want to achieve greatness stop asking for permission.", 194 | "author": "Anonymous", 195 | "id": 28, 196 | "char": 51, 197 | "length": "short" 198 | }, 199 | { 200 | "text": "Success is walking from failure to failure with no loss of enthusiasm.", 201 | "author": "Winston Churchill", 202 | "id": 29, 203 | "char": 59, 204 | "length": "medium" 205 | }, 206 | { 207 | "text": "The harder I work, the luckier I get.", 208 | "author": "Gary Player", 209 | "id": 30, 210 | "char": 30, 211 | "length": "short" 212 | }, 213 | { 214 | "text": "Success is not the absence of failure; it's the persistence through failure.", 215 | "author": "Aisha Tyler", 216 | "id": 31, 217 | "char": 65, 218 | "length": "medium" 219 | }, 220 | { 221 | "text": "The journey of a thousand miles begins with one step.", 222 | "author": "Lao Tzu", 223 | "id": 32, 224 | "char": 44, 225 | "length": "short" 226 | }, 227 | { 228 | "text": "Believe you can, and you're halfway there.", 229 | "author": "Theodore Roosevelt", 230 | "id": 33, 231 | "char": 36, 232 | "length": "short" 233 | }, 234 | { 235 | "text": "If you tell the truth, you don't have to remember anything.", 236 | "author": "Mark Twain", 237 | "id": 34, 238 | "char": 49, 239 | "length": "short" 240 | }, 241 | { 242 | "text": "True terror is to wake up one morning and discover that your high school class is running the country.", 243 | "author": "Kurt Vonnegut", 244 | "id": 35, 245 | "char": 84, 246 | "length": "long" 247 | }, 248 | { 249 | "text": "Always forgive your enemies; nothing annoys them so much.", 250 | "author": "Oscar Wilde", 251 | "id": 36, 252 | "char": 49, 253 | "length": "short" 254 | }, 255 | { 256 | "text": "All that we are is the result of what we have thought.", 257 | "author": "Buddha", 258 | "id": 37, 259 | "char": 43, 260 | "length": "short" 261 | }, 262 | { 263 | "text": "If you judge people, you have no time to love them.", 264 | "author": "Mother Teresa", 265 | "id": 38, 266 | "char": 41, 267 | "length": "short" 268 | }, 269 | { 270 | "text": "The most courageous act is still to think for yourself. Aloud.", 271 | "author": "Coco Chanel", 272 | "id": 39, 273 | "char": 52, 274 | "length": "short" 275 | }, 276 | { 277 | "text": "The greatest wealth is to live content with little.", 278 | "author": "Plato", 279 | "id": 40, 280 | "char": 43, 281 | "length": "short" 282 | }, 283 | { 284 | "text": "The future belongs to those who prepare for it today.", 285 | "author": "Malcolm X", 286 | "id": 41, 287 | "char": 44, 288 | "length": "short" 289 | }, 290 | { 291 | "text": "Be yourself; everyone else is already taken.", 292 | "author": "Oscar Wilde", 293 | "id": 42, 294 | "char": 38, 295 | "length": "short" 296 | }, 297 | { 298 | "text": "I'm selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can't handle me at my worst, then you sure as hell don't deserve me at my best.", 299 | "author": "Marilyn Monroe", 300 | "id": 43, 301 | "char": 162, 302 | "length": "long" 303 | }, 304 | { 305 | "text": "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.", 306 | "author": "Albert Einstein", 307 | "id": 44, 308 | "char": 80, 309 | "length": "long" 310 | }, 311 | { 312 | "text": "So many books, so little time.", 313 | "author": "Frank Zappa", 314 | "id": 45, 315 | "char": 25, 316 | "length": "short" 317 | }, 318 | { 319 | "text": "A room without books is like a body without a soul.", 320 | "author": "Marcus Tullius Cicero", 321 | "id": 46, 322 | "char": 41, 323 | "length": "short" 324 | }, 325 | { 326 | "text": "Don't walk behind me; I may not lead. Don't walk in front of me; I may not follow. Just walk beside me and be my friend.", 327 | "author": "Albert Camus", 328 | "id": 47, 329 | "char": 95, 330 | "length": "long" 331 | }, 332 | { 333 | "text": "If you want to know what a man's like, take a good look at how he treats his inferiors, not his equals.", 334 | "author": "J.K. Rowling", 335 | "id": 48, 336 | "char": 82, 337 | "length": "long" 338 | }, 339 | { 340 | "text": "I have no special talent. I am only passionately curious.", 341 | "author": "Albert Einstein", 342 | "id": 49, 343 | "char": 48, 344 | "length": "short" 345 | }, 346 | { 347 | "text": "It does not matter how slowly you go so long as you do not stop.", 348 | "author": "Confucius", 349 | "id": 50, 350 | "char": 50, 351 | "length": "short" 352 | }, 353 | { 354 | "text": "Early to bed and early to rise makes a man healthy, wealthy, and wise.", 355 | "author": "Benjamin Franklin", 356 | "id": 52, 357 | "char": 57, 358 | "length": "medium" 359 | }, 360 | { 361 | "text": "Family is the most important thing in the world.", 362 | "author": "Diana, Princess of Wales", 363 | "id": 53, 364 | "char": 40, 365 | "length": "short" 366 | }, 367 | { 368 | "text": "All I was doing was trying to get home from work.", 369 | "author": "Rosa Parks", 370 | "id": 54, 371 | "char": 39, 372 | "length": "short" 373 | }, 374 | { 375 | "text": "It is not in the stars to hold our destiny but in ourselves.", 376 | "author": "William Shakespeare", 377 | "id": 55, 378 | "char": 48, 379 | "length": "short" 380 | }, 381 | { 382 | "text": "Never let the fear of striking out keep you.", 383 | "author": "Babe Ruth", 384 | "id": 56, 385 | "char": 36, 386 | "length": "short" 387 | }, 388 | { 389 | "text": "Don't judge each day by the harvest you reap but by the seeds that you plant.", 390 | "author": "Robert Louis Stevenson", 391 | "id": 57, 392 | "char": 62, 393 | "length": "medium" 394 | }, 395 | { 396 | "text": "The most difficult thing is the decision to act, the rest is merely tenacity.", 397 | "author": "Amelia Earhart", 398 | "id": 58, 399 | "char": 64, 400 | "length": "medium" 401 | }, 402 | { 403 | "text": "Life is 10% what happens to you and 90% how you react to it.", 404 | "author": "Charles R. Swindoll", 405 | "id": 59, 406 | "char": 47, 407 | "length": "short" 408 | }, 409 | { 410 | "text": "Start where you are. Use what you have. Do what you can.", 411 | "author": "Arthur Ashe", 412 | "id": 60, 413 | "char": 45, 414 | "length": "short" 415 | }, 416 | { 417 | "text": "Sometimes the road less traveled is less traveled for a reason.", 418 | "author": "Jerry Seinfeld", 419 | "id": 61, 420 | "char": 53, 421 | "length": "short" 422 | }, 423 | { 424 | "text": "My life has no purpose, no direction, no aim, no meaning, and yet I'm happy.", 425 | "author": "Charles Schulz", 426 | "id": 62, 427 | "char": 62, 428 | "length": "medium" 429 | }, 430 | { 431 | "text": "It takes considerable knowledge just to realize the extent of your own ignorance.", 432 | "author": "Thomas Sowell", 433 | "id": 63, 434 | "char": 69, 435 | "length": "medium" 436 | }, 437 | { 438 | "text": "Fairy tales are more than true: not because they tell us that dragons exist, but because they tell us that dragons can be beaten.", 439 | "author": "Neil Gaiman", 440 | "id": 64, 441 | "char": 106, 442 | "length": "long" 443 | }, 444 | { 445 | "text": "Everything you can imagine is real.", 446 | "author": "Pablo Picasso", 447 | "id": 65, 448 | "char": 30, 449 | "length": "short" 450 | }, 451 | { 452 | "text": "Wisely, and slow. They stumble that run fast.", 453 | "author": "William Shakespeare", 454 | "id": 66, 455 | "char": 38, 456 | "length": "short" 457 | }, 458 | { 459 | "text": "Keep calm and carry on.", 460 | "author": "Winston Churchill", 461 | "id": 67, 462 | "char": 19, 463 | "length": "short" 464 | }, 465 | { 466 | "text": "That's one small step for a man, one giant leap for mankind.", 467 | "author": "Neil Armstrong", 468 | "id": 68, 469 | "char": 49, 470 | "length": "short" 471 | }, 472 | { 473 | "text": "I came, I saw, I conquered.", 474 | "author": "Julius Caesar", 475 | "id": 69, 476 | "char": 22, 477 | "length": "short" 478 | }, 479 | { 480 | "text": "I think, therefore I am.", 481 | "author": "René Descartes", 482 | "id": 70, 483 | "char": 20, 484 | "length": "short" 485 | }, 486 | { 487 | "text": "It's not whether you get knocked down, it's whether you get up.", 488 | "author": "Vince Lombardi", 489 | "id": 71, 490 | "char": 52, 491 | "length": "short" 492 | }, 493 | { 494 | "text": "Go confidently in the direction of your dreams. Live the life you have imagined.", 495 | "author": "Henry David Thoreau", 496 | "id": 72, 497 | "char": 67, 498 | "length": "medium" 499 | }, 500 | { 501 | "text": "You miss 100 percent of the shots you never take.", 502 | "author": "Wayne Gretzky", 503 | "id": 73, 504 | "char": 40, 505 | "length": "short" 506 | }, 507 | { 508 | "text": "Nonviolence is a weapon of the strong.", 509 | "author": "Mahatma Gandhi", 510 | "id": 74, 511 | "char": 32, 512 | "length": "short" 513 | }, 514 | { 515 | "text": "Peace begins with a smile.", 516 | "author": "Mother Teresa", 517 | "id": 75, 518 | "char": 22, 519 | "length": "short" 520 | }, 521 | { 522 | "text": "Whenever you find yourself on the side of the majority, it is time to pause and reflect.", 523 | "author": "Mark Twain", 524 | "id": 76, 525 | "char": 72, 526 | "length": "medium" 527 | }, 528 | { 529 | "text": "To be, or not to be, that is the question.", 530 | "author": "William Shakespeare", 531 | "id": 77, 532 | "char": 33, 533 | "length": "short" 534 | }, 535 | { 536 | "text": "Ask not what your country can do for you, but what you can do for your country.", 537 | "author": "John F. Kennedy", 538 | "id": 78, 539 | "char": 63, 540 | "length": "medium" 541 | }, 542 | { 543 | "text": "You've gotta dance like there's nobody watching, love like you'll never be hurt, sing like there's nobody listening, and live like it's heaven on earth.", 544 | "author": "William W. Purkey", 545 | "id": 79, 546 | "char": 128, 547 | "length": "long" 548 | }, 549 | { 550 | "text": "I've learned that people will forget what you said, people will forget what you did, but people will never forget how you made them feel.", 551 | "author": "Maya Angelou", 552 | "id": 80, 553 | "char": 113, 554 | "length": "long" 555 | }, 556 | { 557 | "text": "The only thing we have to fear is fear itself.", 558 | "author": "Franklin D. Roosevelt", 559 | "id": 81, 560 | "char": 37, 561 | "length": "short" 562 | }, 563 | { 564 | "text": "The unexamined life is not worth living.", 565 | "author": "Socrates", 566 | "id": 82, 567 | "char": 34, 568 | "length": "short" 569 | }, 570 | { 571 | "text": "Be the change that you wish to see in the world.", 572 | "author": "Mahatma Gandhi", 573 | "id": 83, 574 | "char": 38, 575 | "length": "short" 576 | }, 577 | { 578 | "text": "The mind is like a parachute. It doesn't work if it is not open.", 579 | "author": "Frank Zappa", 580 | "id": 84, 581 | "char": 51, 582 | "length": "short" 583 | }, 584 | { 585 | "text": "The successful warrior is the average man, with laser-like focus.", 586 | "author": "Bruce Lee", 587 | "id": 85, 588 | "char": 56, 589 | "length": "medium" 590 | }, 591 | { 592 | "text": "Those who dare to fail miserably can achieve greatly.", 593 | "author": "John F. Kennedy", 594 | "id": 86, 595 | "char": 45, 596 | "length": "short" 597 | }, 598 | { 599 | "text": "I've failed over and over and over again in my life and that is why I succeed.", 600 | "author": "Michael Jordan", 601 | "id": 87, 602 | "char": 62, 603 | "length": "medium" 604 | }, 605 | { 606 | "text": "You know you're in love when you can't fall asleep because reality is finally better than your dreams.", 607 | "author": "Dr. Seuss", 608 | "id": 88, 609 | "char": 85, 610 | "length": "long" 611 | }, 612 | { 613 | "text": "If you tell the truth, you don't have to remember anything.", 614 | "author": "Mark Twain", 615 | "id": 89, 616 | "char": 49, 617 | "length": "short" 618 | }, 619 | { 620 | "text": "Friendship... is born at the moment when one man says to another \"What! You too? I thought that no one but myself...\"", 621 | "author": "C.S. Lewis", 622 | "id": 90, 623 | "char": 96, 624 | "length": "long" 625 | }, 626 | { 627 | "text": "The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", 628 | "author": "Helen Keller", 629 | "id": 91, 630 | "char": 92, 631 | "length": "long" 632 | }, 633 | { 634 | "text": "The only way to do great work is to love what you do. If you haven't found it yet, keep looking. Don't settle.", 635 | "author": "Steve Jobs", 636 | "id": 92, 637 | "char": 88, 638 | "length": "long" 639 | }, 640 | { 641 | "text": "The mind is everything. What you think, you become.", 642 | "author": "Buddha", 643 | "id": 93, 644 | "char": 43, 645 | "length": "short" 646 | }, 647 | { 648 | "text": "The only thing that stands between you and your dream is the will to try and the belief that it is actually possible.", 649 | "author": "Joel Brown", 650 | "id": 94, 651 | "char": 95, 652 | "length": "long" 653 | }, 654 | { 655 | "text": "The greatest glory in living lies not in never falling, but in rising every time we fall.", 656 | "author": "Nelson Mandela", 657 | "id": 95, 658 | "char": 73, 659 | "length": "medium" 660 | }, 661 | { 662 | "text": "The only thing that is impossible is the thing you don't try.", 663 | "author": "George Bernard Shaw", 664 | "id": 96, 665 | "char": 50, 666 | "length": "short" 667 | }, 668 | { 669 | "text": "The only way to achieve the impossible is to believe it is possible.", 670 | "author": "Alice in Wonderland", 671 | "id": 97, 672 | "char": 56, 673 | "length": "medium" 674 | }, 675 | { 676 | "text": "The only way to overcome fear is to face it head-on.", 677 | "author": "Susan Jeffers", 678 | "id": 98, 679 | "char": 42, 680 | "length": "short" 681 | }, 682 | { 683 | "text": "The only way to achieve success is to work hard and never give up.", 684 | "author": "Unknown", 685 | "id": 99, 686 | "char": 53, 687 | "length": "short" 688 | }, 689 | { 690 | "text": "The only way to be happy is to be content with what you have.", 691 | "author": "Epicurus", 692 | "id": 100, 693 | "char": 48, 694 | "length": "short" 695 | }, 696 | { 697 | "text": "The only way to make a difference in the world is to take action.", 698 | "author": "Mahatma Gandhi", 699 | "id": 101, 700 | "char": 52, 701 | "length": "short" 702 | }, 703 | { 704 | "text": "The only way to live a meaningful life is to follow your dreams.", 705 | "author": "Jim Rohn", 706 | "id": 102, 707 | "char": 52, 708 | "length": "short" 709 | }, 710 | { 711 | "text": "The only way to be successful is to be yourself.", 712 | "author": "Beyonce", 713 | "id": 103, 714 | "char": 39, 715 | "length": "short" 716 | }, 717 | { 718 | "text": "The only way to be happy is to be grateful.", 719 | "author": "Melody Beattie", 720 | "id": 104, 721 | "char": 34, 722 | "length": "short" 723 | }, 724 | { 725 | "text": "The only way to make the world a better place is to start with yourself.", 726 | "author": "Malala Yousafzai", 727 | "id": 105, 728 | "char": 58, 729 | "length": "medium" 730 | }, 731 | { 732 | "text": "The only way to achieve greatness is to never stop learning.", 733 | "author": "Albert Einstein", 734 | "id": 106, 735 | "char": 50, 736 | "length": "short" 737 | }, 738 | { 739 | "text": "The best way to find yourself is to lose yourself in the service of others.", 740 | "author": "Mahatma Gandhi", 741 | "id": 107, 742 | "char": 61, 743 | "length": "medium" 744 | }, 745 | { 746 | "text": "Happiness is not something ready made. It comes from your own actions.", 747 | "author": "Dalai Lama", 748 | "id": 108, 749 | "char": 59, 750 | "length": "medium" 751 | }, 752 | { 753 | "text": "If you can dream it, you can do it.", 754 | "author": "Walt Disney", 755 | "id": 109, 756 | "char": 27, 757 | "length": "short" 758 | }, 759 | { 760 | "text": "It's not how much you have, but how much you enjoy that makes happiness.", 761 | "author": "Charles Spurgeon", 762 | "id": 110, 763 | "char": 59, 764 | "length": "medium" 765 | }, 766 | { 767 | "text": "The greatest gift you can give is your time.", 768 | "author": "Jim Rohn", 769 | "id": 111, 770 | "char": 36, 771 | "length": "short" 772 | }, 773 | { 774 | "text": "The best way to make friends is to be one.", 775 | "author": "Ralph Waldo Emerson", 776 | "id": 112, 777 | "char": 33, 778 | "length": "short" 779 | }, 780 | { 781 | "text": "The more you learn, the more you earn.", 782 | "author": "Benjamin Franklin", 783 | "id": 113, 784 | "char": 31, 785 | "length": "short" 786 | } 787 | ] 788 | } 789 | --------------------------------------------------------------------------------