diff --git a/ccna4-missing b/ccna4-missing new file mode 100644 index 0000000..70ba21c --- /dev/null +++ b/ccna4-missing @@ -0,0 +1,8 @@ +'21 | Next is not UL but: undefined', +'52 | No correct answers found!', +'98 | Next is not UL but: undefined', +'99 | Next is not UL but: undefined', +'100 | Next is not UL but: undefined', +'143 | Next is not UL but: undefined', +'144 | Next is not UL but: undefined', +'145 | No correct answers found!' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8453f0a..409afc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1038,6 +1038,12 @@ "physical-cpu-count": "^2.0.0" } }, + "@types/elasticlunr": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/elasticlunr/-/elasticlunr-0.9.0.tgz", + "integrity": "sha512-7xUGaa0HqDmfawyFd8ANaoTHt5KF/BEdvKfqz4NxGEhbloIXGGtH065MOvC+YYQRZPAA/b5Vt/Xe5AmXoKTmhQ==", + "dev": true + }, "@types/navigo": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@types/navigo/-/navigo-7.0.1.tgz", @@ -2732,6 +2738,11 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "elasticlunr": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/elasticlunr/-/elasticlunr-0.9.5.tgz", + "integrity": "sha1-ZVQbswnd3Qz5Ty0ciGGyvmUbsNU=" + }, "electron-to-chromium": { "version": "1.3.314", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.314.tgz", diff --git a/package.json b/package.json index a89f5de..36fdfcf 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@hibas123/theme": "^1.2.14", "@hibas123/utils": "^2.2.3", + "elasticlunr": "^0.9.5", "feather-icons": "^4.24.1", "navigo": "^7.1.2", "svelte": "^3.16.0", @@ -19,6 +20,7 @@ "uuid": "^3.3.3" }, "devDependencies": { + "@types/elasticlunr": "^0.9.0", "@types/navigo": "^7.0.1", "@types/node": "^12.12.14", "@types/uuid": "^3.4.6", diff --git a/src/App.svelte b/src/App.svelte index 418d25b..84ee45e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,28 +1,40 @@ -
- {#if $DeviceType === DeviceTypes.MOBILE} - - {:else} - - {/if} + - {#if $ModalStore} - - {/if} -
\ No newline at end of file +
+
+ (window.location.hash = '#/')}>CCNA Trainer: + {version} +
+ {#if $DeviceType === DeviceTypes.MOBILE} + + {:else} + + {/if} + + {#if $ModalStore} + + {/if} +
+ +
{version}
diff --git a/src/main.ts b/src/main.ts index 0ab78d1..7bf02b1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -43,4 +43,8 @@ const app = new App({ target: document.getElementById("content") }); -export default app; \ No newline at end of file +export default app; + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register("sw.ts"); +} \ No newline at end of file diff --git a/src/router.ts b/src/router.ts index 3c0f767..fb3abab 100644 --- a/src/router.ts +++ b/src/router.ts @@ -19,14 +19,28 @@ export const pageStore = readable(undefined, (set) => { router.resolve(); }); +import Home from "./views/Home.svelte" import Overview from "./views/Overview.svelte"; +import Browse from "./views/Browse.svelte"; router.on("/", (params) => { + setComponent({ + desktop: Home as typeof SvelteComponent, + mobile: Home as typeof SvelteComponent, + params: {} + }) +}).on("/test", (params) => { setComponent({ desktop: Overview as typeof SvelteComponent, mobile: Overview as typeof SvelteComponent, params }) +}).on("/browse", (params) => { + setComponent({ + desktop: Browse as typeof SvelteComponent, + mobile: Browse as typeof SvelteComponent, + params: {} + }) }) // .on("/:vaultid", (params) => { diff --git a/src/stores.ts b/src/stores.ts index c0bf807..7c36f5f 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -1,28 +1,35 @@ import { writable, readable } from "svelte/store"; -import { Question, Exam } from "./data"; +import { Question, Exam, QuestionTypes } from "./data"; import CCNA2 from "../ccna2.json"; import CCNA3 from "../ccna3.json"; import CCNA4 from "../ccna4.json"; import TestData from "../data-test.json"; -const tests = new Map(); -tests.set("ccna4", CCNA4); -tests.set("ccna3", CCNA3); -tests.set("ccna2", CCNA2); -tests.set("test", TestData) +const Tests = new Map(); +Tests.set("ccna4", CCNA4); +Tests.set("ccna3", CCNA3); +Tests.set("ccna2", CCNA2); +Tests.set("test", TestData) const DEFAULT = "ccna4"; -let test = new URL(window.location.href).searchParams.get("exam") || DEFAULT; +export let test = new URL(window.location.href).searchParams.get("exam") || DEFAULT; -const dataVersion = tests.has(test) ? test : DEFAULT; -const Data = tests.get(dataVersion); +const dataVersion = Tests.has(test) ? test : DEFAULT; +const Data = Tests.get(dataVersion); +const QuestionMap = new Map(); + +Data.questions.forEach(q => QuestionMap.set(q.id, q)); console.log("Running exam:", dataVersion); const runsShould = 3; +import elasticlunr from "elasticlunr"; + +console.log(elasticlunr); + class QuestionManager { version: string = dataVersion; @@ -40,6 +47,22 @@ class QuestionManager { level: number }[]; + index: elasticlunr.Index<{ id: string, title: string, answers: string }>; + + private getAnswerString(question: Question) { + switch (question.type) { + case QuestionTypes.SelectOne: + case QuestionTypes.SelectMultiple: + return Object.values(question.options).join(" ; "); + case QuestionTypes.TextInput: + return question.correct; + case QuestionTypes.AssignValues: + return [...Object.values(question.fields), ...Object.values(question.values)].join(" ; "); + default: + return ""; + } + } + constructor() { this.availableQuestions = Data.questions.map(question => { return { @@ -49,6 +72,32 @@ class QuestionManager { }) this.getNewActive(); + + this.index = elasticlunr(function () { + this.addField("title") + this.addField("answers"); + this.setRef("id") + }) + + Data.questions + .map(e => ({ + id: e.id, + title: e.title, + answers: this.getAnswerString(e) + })) + .forEach(e => this.index.addDoc(e)); + } + + search(term: string) { + if (term === "") + return Data.questions; + const match = this.index.search(term, { + fields: { + title: { boost: 2 }, + answers: { boost: 1 } + } + }).map(e => QuestionMap.get(e.ref)); + return match; } getNewActive() { @@ -108,4 +157,6 @@ export const IsDark = readable(Theme.isDark(), set => { return () => Theme.isDarkObservable.unsubscribe(onChange); }) -export { ModalStore } from "./modals"; \ No newline at end of file +export { ModalStore } from "./modals"; + +export const AvailableTests = Array.from(Tests.keys()); \ No newline at end of file diff --git a/src/sw.ts b/src/sw.ts new file mode 100644 index 0000000..08f4b40 --- /dev/null +++ b/src/sw.ts @@ -0,0 +1,37 @@ +var CACHE = 'cache-and-update'; + +self.addEventListener('install', function (evt) { + console.log('The service worker is being installed.'); + evt.waitUntil(precache()); +}); + +self.addEventListener('fetch', function (evt: FetchEvent) { + console.log('The service worker is serving the asset.'); + evt.respondWith(fromCache(evt.request, update(evt.request))); + evt.waitUntil(update(evt.request)); +}); + +function precache() { + return caches.open(CACHE).then(function (cache) { + return cache.addAll([ + "./", + "./index.html" + ]); + }); +} + +function fromCache(request, update) { + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + return matching || update; + }); + }); +} + +function update(request) { + return caches.open(CACHE).then(function (cache) { + return fetch(request).then(function (response) { + return cache.put(request, response); + }); + }); +} \ No newline at end of file diff --git a/src/views/Browse.svelte b/src/views/Browse.svelte new file mode 100644 index 0000000..309062b --- /dev/null +++ b/src/views/Browse.svelte @@ -0,0 +1,78 @@ + + +
+
+ + +
    + {#each questions as question} +
  • opened = opened === question.id ? undefined : question.id}> +

    + {@html question.title} +

    + {#if opened === question.id} + {#if question.type === QuestionTypes.SelectOne} +
      + {#each Object.entries(question.options) as [key, value]} +
    • + {value} +
    • + {/each} +
    + {:else if question.type === QuestionTypes.SelectMultiple} +
      + {#each Object.entries(question.options) as [key, value]} +
    • = 0 ? "correct" : ""}> + {value} +
    • + {/each} +
    + {:else if question.type === QuestionTypes.TextInput} +

    {question.correct}

    + {:else if question.type === QuestionTypes.AssignValues} +
      + {#each Object.entries(question.fields) as [key, value]} +
    • + {value} ➞ {question.values[question.correct[key]]} +
    • + {/each} +
    + {:else} + Unknown type! + {/if} + {/if} +
  • + {/each} +
+
+ + + + \ No newline at end of file diff --git a/src/views/Home.svelte b/src/views/Home.svelte new file mode 100644 index 0000000..ed5e8a4 --- /dev/null +++ b/src/views/Home.svelte @@ -0,0 +1,20 @@ + + +
+

Test your knowlegde

+ +

Browse and search specific questions

+ + +
+ + +
\ No newline at end of file diff --git a/src/views/Overview.svelte b/src/views/Overview.svelte index 674b978..e6ec696 100644 --- a/src/views/Overview.svelte +++ b/src/views/Overview.svelte @@ -1,8 +1,7 @@