spanish-quizzer

An app to quiz you on Spanish vocabulary and verb conjugations
git clone https://git.ashermorgan.net/spanish-quizzer/
Log | Files | Refs | README

commit 1f9d631258cbeb0b6fbb309ea588892fe0c5b364
parent 93563039001002d00033e9062817c594f8c83ae0
Author: AsherMorgan <59518073+AsherMorgan@users.noreply.github.com>
Date:   Fri, 12 Mar 2021 18:14:10 -0800

Create /settings page

Diffstat:
Mcss/filtersPage.css | 31++++++++++++-------------------
Mcss/global.css | 15+++++++++++----
Acss/settingsPage.css | 26++++++++++++++++++++++++++
Aimages/settings.svg | 2++
Mindex.html | 2++
Mjs/app.js | 38++++++++++++++++++++++++++++++++++----
Mjs/filtersPage.js | 112++++++++++---------------------------------------------------------------------
Mjs/quizzer.js | 4++--
Mjs/reference.js | 2+-
Ajs/settingsPage.js | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mservice-worker.js | 4+++-
Mtests/index.html | 6++++--
Mtests/test.filtersPage.js | 64----------------------------------------------------------------
Atests/test.settingsPage.js | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 286 insertions(+), 195 deletions(-)

diff --git a/css/filtersPage.css b/css/filtersPage.css @@ -47,25 +47,6 @@ -/******** settings-input component ********/ -.settingsInput h2 { - text-align: center; - margin-bottom: 5px; -} -.settingsInput div, .settingsInput h3 { - text-align: left; - margin-bottom: 5px; -} -.settingsInput h3 { - margin-top: 10px; - font-size: 16px; -} -.settingsInput input[type=number] { - width: 50px; -} - - - /******** filters-page component ********/ .filtersPage main { display: flex; @@ -96,3 +77,15 @@ height: 50px; } } +@media only screen and (max-width: 800px) { + /* Hide settings */ + .filtersPage main .settingsInput { + display: none; + } +} +@media only screen and (min-width: 801px) { + /* Hide settings icon */ + .filtersPage header img:last-child { + display: none; + } +} diff --git a/css/global.css b/css/global.css @@ -36,19 +36,26 @@ header { background-color: var(--theme-color); font-size: 25px; font-weight: bold; - cursor: pointer; color: #000000; text-decoration: none; margin: 0px; padding-top: 15px; padding-bottom: 15px; } -header img { +header>* { + cursor: pointer; +} +header>img { position: absolute; - left: 10px; height: 30px; background-color: var(--theme-color); } +header>img:first-child { + left: 10px; +} +header>img:last-child { + right: 10px; +} @@ -66,7 +73,7 @@ header img { /******** Other elements ********/ -h1, h2 { +h1 { font-size: 20px; } button:not(.icon) { diff --git a/css/settingsPage.css b/css/settingsPage.css @@ -0,0 +1,26 @@ +/******** settings-input component ********/ +.settingsInput h1 { + text-align: center; + margin-bottom: 5px; +} +.settingsInput div, .settingsInput h2 { + text-align: left; + margin-bottom: 5px; +} +.settingsInput h2 { + margin-top: 10px; + font-size: 16px; +} +.settingsInput input[type=number] { + width: 50px; +} + + + +/******** settings-page component ********/ +.settingsPage main { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px; +} diff --git a/images/settings.svg b/images/settings.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> +\ No newline at end of file diff --git a/index.html b/index.html @@ -12,6 +12,7 @@ <link rel="stylesheet" href="css/global.css"> <link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/filtersPage.css"> + <link rel="stylesheet" href="css/settingsPage.css"> <link rel="stylesheet" href="css/quizzer.css"> <link rel="stylesheet" href="css/reference.css"> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> @@ -20,6 +21,7 @@ <script src="js/global.js"></script> <script src="js/filters.js"></script> <script src="js/filtersPage.js"></script> + <script src="js/settingsPage.js"></script> <script src="js/quizzer.js"></script> <script src="js/reference.js"></script> <script src="js/app.js"></script> diff --git a/js/app.js b/js/app.js @@ -6,7 +6,10 @@ let app; // page-header component const pageHeader = Vue.component("pageHeader", { props: { - image: { + icon1: { + type: String + }, + icon2: { type: String }, title: { @@ -14,10 +17,26 @@ const pageHeader = Vue.component("pageHeader", { default: "Spanish-Quizzer", } }, + computed: { + image1: function() { + if (this.icon1) return `images/${this.icon1}.svg`; + else return null; + }, + image2: function() { + if (this.icon2) return `images/${this.icon2}.svg`; + else return null; + } + }, + methods: { + goHome: function() { + if (this.$route.name !== 'home') this.$router.push('home'); + } + }, template: ` - <header @click="$emit('back');"> - <img v-if="image" :src="image"/> - {{ title }} + <header> + <img v-if="image1" :src="image1" @click="$emit('click1')"/> + <span @click="goHome">{{ title }}</span> + <img v-if="image2" :src="image2" @click="$emit('click2')"/> </header> ` }); @@ -113,6 +132,13 @@ function loadVue() { props: { category: "vocab" } }, { + path: "/settings", + name: "settings", + meta: { title: "Settings" }, + component: settingsPage, + props: true + }, + { path: "/quizzer", name: "quizzer", meta: { title: "Quizzer" }, @@ -145,8 +171,12 @@ function loadVue() { case "reference": this.$router.push("home"); break; + case "settings": + this.$router.push(this.$route.params.referer || "home"); + break; case "quizzer": this.$router.push(this.$route.params.referer || "home"); + break; } }, diff --git a/js/filtersPage.js b/js/filtersPage.js @@ -1,5 +1,5 @@ // filter-input component -let filterInput = Vue.component("filterInput", { +const filterInput = Vue.component("filterInput", { props: { category: { type: String, @@ -213,10 +213,10 @@ let filterInput = Vue.component("filterInput", { template: ` <div class="filtersInput" ref="container"> <div class="verbSettings" v-show="category === 'verbs'"> - <h2> + <h1> Verb Filters <button class="icon" @click="AddFilter();"><img src="./images/plus.svg"></button> - </h2> + </h1> <div v-for="(filter, index) in verbFilters" class="filter"> <select v-model="filter.tense"> @@ -247,10 +247,10 @@ let filterInput = Vue.component("filterInput", { <div class="vocabSettings" v-show="category === 'vocab'"> - <h2> + <h1> Vocabulary Filters <button class="icon" @click="AddFilter();"><img src="./images/plus.svg"></button> - </h2> + </h1> <div v-for="(filter, index) in vocabFilters" class="filter"> <select class="vocabSetName" v-model="filter.category"> @@ -299,99 +299,8 @@ let filterInput = Vue.component("filterInput", { -// settings-input component -let settingsInput = Vue.component("settingsInput", { - props: { - value: { - type: Object, - default: getSettings(), - }, - }, - - watch: { - value: { - handler: function(value) { - setSettings(value); - - this.$emit("input", value); - }, - deep: true, - }, - }, - - template: ` - <div class="settingsInput" ref="container"> - <h2>Settings</h2> - <h3>Appearance</h3> - <div> - <input type="checkbox" id="settingsDarkTheme" v-model="value.darkTheme"> - <label for="settingsDarkTheme">Dark Mode</label> - </div> - - <h3>Prompts</h3> - <div> - <label for="settingsPromptType">Prompt type</label> - <select id="settingsPromptType" v-model="value.promptType"> - <option>Text</option> - <option>Audio</option> - <option>Both</option> - </select> - </div> - <div> - <label for="settingsInputType">Input type</label> - <select id="settingsInputType" v-model="value.inputType"> - <option>Text</option> - <option>Voice</option> - <option>Either</option> - </select> - </div> - <div> - <label for="settingsMultiplePrompts">Multiple prompts</label> - <select id="settingsMultiplePrompts" v-model="value.multiplePrompts"> - <option>Show together</option> - <option>Show separately</option> - <option>Show one</option> - </select> - </div> - <div> - <input type="checkbox" id="settingsRemoveDuplicates" v-model="value.removeDuplicates"> - <label for="settingsRemoveDuplicates">Remove duplicate prompts</label> - </div> - - <h3>Grading</h3> - <div> - <label for="settingsOnMissedPrompt">When I miss a prompt</label> - <select id="settingsOnMissedPrompt" v-model="value.onMissedPrompt"> - <option>Correct me</option> - <option>Tell me</option> - <option>Ignore it</option> - </select> - </div> - <div> - <label for="settingsRepeatPrompts">Repeat missed prompts</label> - <select id="settingsRepeatPrompts" v-model="value.repeatPrompts"> - <option>Never</option> - <option>Immediately</option> - <option>5 prompts later</option> - <option>5 & 10 prompts later</option> - <option>At the end</option> - </select> - </div> - <div> - <label for="settingsMultipleAnswers">Multiple answers</label> - <select id="settingsMultipleAnswers" v-model="value.multipleAnswers"> - <option>Require all</option> - <option>Require any</option> - </select> - </div> - </div> - `, -}); - - - // filters-page component -let filtersPage = Vue.component("filtersPage", { +const filtersPage = Vue.component("filtersPage", { props: { category: { type: String, @@ -450,6 +359,13 @@ let filtersPage = Vue.component("filtersPage", { }, /** + * Open the settings page + */ + openSettings: function() { + this.$router.push({name:"settings", params:{referer:this.category}}); + }, + + /** * Handle a keyup event (implements some keyboard shortcuts). * @param {object} e - The event args. */ @@ -476,7 +392,7 @@ let filtersPage = Vue.component("filtersPage", { template: ` <div class="filtersPage"> - <page-header @back="$emit('back');" image="images/arrow-left.svg"></page-header> + <page-header icon1="arrow-left" @click1="$emit('back');" icon2="settings" @click2="openSettings"></page-header> <main> <filter-input ref="filters" :category="category" v-model="filters"></filter-input> <settings-input v-model="settings"></settings-input> diff --git a/js/quizzer.js b/js/quizzer.js @@ -1,4 +1,4 @@ -let quizzer = Vue.component("quizzer", { +const quizzer = Vue.component("quizzer", { props: { startingPrompts: { type: Array, @@ -378,7 +378,7 @@ const quizzerPage = Vue.component("quizzerPage", { template: ` <div class="quizzer-page"> - <page-header @back="$emit('back', referer);" image="images/x.svg"></page-header> + <page-header @click1="$emit('back', referer);" icon1="x"></page-header> <main> <quizzer :starting-prompts="prompts" :starting-index="index" :settings="settings" @new-prompt="updateProgress" @finished-prompts="$emit('back', referer);"> diff --git a/js/reference.js b/js/reference.js @@ -109,7 +109,7 @@ const referenceTables = Vue.component("referenceTables", { const referencePage = Vue.component("referencePage", { template: ` <div class="referencePage"> - <page-header @back="$emit('back');" image="images/arrow-left.svg"></page-header> + <page-header @click1="$emit('back');" icon1="arrow-left"></page-header> <main> <reference-tables :data="this.$root.$data.data"></reference-tables> </main> diff --git a/js/settingsPage.js b/js/settingsPage.js @@ -0,0 +1,114 @@ +// settings-input component +const settingsInput = Vue.component("settingsInput", { + props: { + value: { + type: Object, + default: getSettings(), + }, + }, + + watch: { + value: { + handler: function(value) { + setSettings(value); + + this.$emit("input", value); + }, + deep: true, + }, + }, + + activated: function() { + // Refresh settings + this.value = getSettings(); + }, + + template: ` + <div class="settingsInput" ref="container"> + <h1>Settings</h1> + <h2>Appearance</h2> + <div> + <input type="checkbox" id="settingsDarkTheme" v-model="value.darkTheme"> + <label for="settingsDarkTheme">Dark Mode</label> + </div> + + <h2>Quizzer Prompts</h2> + <div> + <label for="settingsPromptType">Prompt type</label> + <select id="settingsPromptType" v-model="value.promptType"> + <option>Text</option> + <option>Audio</option> + <option>Both</option> + </select> + </div> + <div> + <label for="settingsInputType">Input type</label> + <select id="settingsInputType" v-model="value.inputType"> + <option>Text</option> + <option>Voice</option> + <option>Either</option> + </select> + </div> + <div> + <label for="settingsMultiplePrompts">Multiple prompts</label> + <select id="settingsMultiplePrompts" v-model="value.multiplePrompts"> + <option>Show together</option> + <option>Show separately</option> + <option>Show one</option> + </select> + </div> + <div> + <input type="checkbox" id="settingsRemoveDuplicates" v-model="value.removeDuplicates"> + <label for="settingsRemoveDuplicates">Remove duplicate prompts</label> + </div> + + <h2>Quizzer Grading</h2> + <div> + <label for="settingsOnMissedPrompt">When I miss a prompt</label> + <select id="settingsOnMissedPrompt" v-model="value.onMissedPrompt"> + <option>Correct me</option> + <option>Tell me</option> + <option>Ignore it</option> + </select> + </div> + <div> + <label for="settingsRepeatPrompts">Repeat missed prompts</label> + <select id="settingsRepeatPrompts" v-model="value.repeatPrompts"> + <option>Never</option> + <option>Immediately</option> + <option>5 prompts later</option> + <option>5 & 10 prompts later</option> + <option>At the end</option> + </select> + </div> + <div> + <label for="settingsMultipleAnswers">Multiple answers</label> + <select id="settingsMultipleAnswers" v-model="value.multipleAnswers"> + <option>Require all</option> + <option>Require any</option> + </select> + </div> + </div> + `, +}); + + + +// settings-page component +const settingsPage = Vue.component("settingsPage", { + props: { + referer: { + type: String, + default: "home", + }, + }, + + template: ` + <div class="settingsPage"> + <page-header @click1="$emit('back', referer);" icon1="x"></page-header> + <main> + <settings-input></settings-input> + </main> + </div> + `, +}); diff --git a/service-worker.js b/service-worker.js @@ -1,5 +1,5 @@ // Initialize constants -const version = "spanish-quizzer-1"; +const version = "spanish-quizzer-2"; const resources = [ "https://cdn.jsdelivr.net/npm/vue@2.6.12", "https://cdn.jsdelivr.net/npm/vue-router@3.5.1", @@ -9,6 +9,7 @@ const resources = [ "./css/global.css", "./css/quizzer.css", "./css/reference.css", + "./css/settingsPage.css", "./data/verbs.csv", "./data/vocab.csv", "./images/arrow-left.svg", @@ -27,6 +28,7 @@ const resources = [ "./js/global.js", "./js/quizzer.js", "./js/reference.js", + "./js/settingsPage.js", "./index.html", "./", ]; diff --git a/tests/index.html b/tests/index.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8" /> - <title>Mocha Tests - Spanish-Quizzer</title> + <title>Tests - Spanish-Quizzer</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="icon" type="image/png" href="../images/favicon-32.png"> <link rel="apple-touch-icon" sizes="180x180" href="../images/favicon-180.png"> @@ -18,12 +18,13 @@ <!-- Scripts being tested --> <script src="../js/global.js"></script> <script src="../js/filtersPage.js"></script> + <script src="../js/settingsPage.js"></script> <script src="../js/filters.js"></script> <script src="../js/quizzer.js"></script> <!-- Setup tests --> <script class="mocha-init"> - mocha.setup('bdd'); + mocha.setup("bdd"); mocha.checkLeaks(); let expect = chai.expect; </script> @@ -31,6 +32,7 @@ <!-- Tests --> <script src="test.global.js"></script> <script src="test.filtersPage.js"></script> + <script src="test.settingsPage.js"></script> <script src="test.filters.js"></script> <script src="test.quizzer.js"></script> diff --git a/tests/test.filtersPage.js b/tests/test.filtersPage.js @@ -384,70 +384,6 @@ describe("FilterInput", function() { -// settings-input component -describe("SettingsInput", function() { - let SettingsInput; - beforeEach(function() { - // Create settingsInput component - SettingsInput = new settingsInput(); - }); - - describe("Value watch", function() { - it("Should emit input event", async function() { - // Override $emit method - let event = ""; - let event_args; - SettingsInput.$emit = function(name, value) { - event = name; - event_args = value; - }; - - // Override setSettings method - let old_setSettings = setSettings; - setSettings = function() {}; - - // Edit setting - SettingsInput.value.promptType = "test-prompt-type"; - await SettingsInput.$nextTick(); - - // Assert event emited - expect(event).to.equal("input"); - expect(event_args.promptType).to.equal("test-prompt-type"); - - // Restore setSettings method - setSettings = old_setSettings; - }); - - it("Should call setSettings", async function() { - // Override $emit method - let event = ""; - let event_args; - SettingsInput.$emit = function(name, value) { - event = name; - event_args = value; - }; - - // Override setSettings method - let old_setSettings = setSettings; - let args = null; - setSettings = function(value) { - args = value; - }; - - // Edit setting - SettingsInput.value.inputType = "test-input-type"; - await SettingsInput.$nextTick(); - // Assert setSettings called - expect(args.inputType).to.equal("test-input-type"); - - // Restore setSettings method - setSettings = old_setSettings; - }); - }); -}); - - - // filters-page component describe("FiltersPage", function() { let FiltersPage; diff --git a/tests/test.settingsPage.js b/tests/test.settingsPage.js @@ -0,0 +1,61 @@ +// settings-input component +describe("SettingsInput", function() { + let SettingsInput; + beforeEach(function() { + // Create settingsInput component + SettingsInput = new settingsInput(); + }); + + describe("Value watch", function() { + it("Should emit input event", async function() { + // Override $emit method + let event = ""; + let event_args; + SettingsInput.$emit = function(name, value) { + event = name; + event_args = value; + }; + + // Override setSettings method + let old_setSettings = setSettings; + setSettings = function() {}; + + // Edit setting + SettingsInput.value.promptType = "test-prompt-type"; + await SettingsInput.$nextTick(); + + // Assert event emited + expect(event).to.equal("input"); + expect(event_args.promptType).to.equal("test-prompt-type"); + + // Restore setSettings method + setSettings = old_setSettings; + }); + + it("Should call setSettings", async function() { + // Override $emit method + let event = ""; + let event_args; + SettingsInput.$emit = function(name, value) { + event = name; + event_args = value; + }; + + // Override setSettings method + let old_setSettings = setSettings; + let args = null; + setSettings = function(value) { + args = value; + }; + + // Edit setting + SettingsInput.value.inputType = "test-input-type"; + await SettingsInput.$nextTick(); + // Assert setSettings called + expect(args.inputType).to.equal("test-input-type"); + + // Restore setSettings method + setSettings = old_setSettings; + }); + }); +});