From a938178262ce1aba1b4a4b679f679660442c5fd9 Mon Sep 17 00:00:00 2001 From: 100gle Date: Sun, 24 Dec 2023 22:06:40 +0800 Subject: [PATCH 01/17] Add passed status manager --- static/js/passedState.js | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 static/js/passedState.js diff --git a/static/js/passedState.js b/static/js/passedState.js new file mode 100644 index 0000000..95bec9e --- /dev/null +++ b/static/js/passedState.js @@ -0,0 +1,43 @@ + + +class PassedState { + constructor(initialState = {}) { + const currentState = localStorage.getItem('passedState'); + // initialize the state when there is no state in the local storage. + if (!currentState) { + if (!initialState) { + throw new Error('initial state is required when there is no state in the local storage.'); + } + + const state = this._prepareState(initialState); + localStorage.setItem('passedState', JSON.stringify(state)); + this.state = state; + return; + } + + if (currentState) { + this.state = JSON.parse(currentState); + } + } + + /** + * prepare the state for initialization. + * @param {object} rawState + * @returns state - the state contains the challenge name and whether the challenge is passed. + */ + _prepareState(rawState) { + const state = {}; + for (const level in rawState) { + const challenges = []; + for (const challengeName of rawState[level]) { + challenges.push({ + name: challengeName, + passed: false + }); + } + state[level] = challenges; + } + + return state; + } +} \ No newline at end of file From 4684a29c2388ac3d9067643189fe3336e8428c9e Mon Sep 17 00:00:00 2001 From: 100gle Date: Sun, 24 Dec 2023 22:44:10 +0800 Subject: [PATCH 02/17] Add basic method for passedState --- static/js/passedState.js | 75 +++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/static/js/passedState.js b/static/js/passedState.js index 95bec9e..619c36a 100644 --- a/static/js/passedState.js +++ b/static/js/passedState.js @@ -1,23 +1,25 @@ class PassedState { + + _key = 'passedState'; + state = null; + constructor(initialState = {}) { - const currentState = localStorage.getItem('passedState'); + const currentState = localStorage.getItem(this._key); // initialize the state when there is no state in the local storage. - if (!currentState) { - if (!initialState) { - throw new Error('initial state is required when there is no state in the local storage.'); - } - - const state = this._prepareState(initialState); - localStorage.setItem('passedState', JSON.stringify(state)); - this.state = state; - return; + if (!currentState && !initialState) { + throw new Error('initial state is required when there is no state in the local storage.'); } - if (currentState) { - this.state = JSON.parse(currentState); - } + // add passed property to the challenges in the initial state. + const rawState = this._prepareState(initialState); + + // check new state and old state whether is undefined or not. and merge the new state to the old state. + const state = this._checkAndMerge(rawState, currentState); + this._save(state); + this.state = state; + return } /** @@ -26,6 +28,10 @@ class PassedState { * @returns state - the state contains the challenge name and whether the challenge is passed. */ _prepareState(rawState) { + if (!rawState) { + return {}; + } + const state = {}; for (const level in rawState) { const challenges = []; @@ -40,4 +46,47 @@ class PassedState { return state; } + + get() { + return this.state; + } + + _save(state) { + localStorage.setItem(this._key, JSON.stringify(state)); + } + + /** + * Set the challenge as passed in the state. + * @param {'basic' | 'intermediate' | 'advanced' | 'extreme'} level - the level of the challenge. + * @param {string} challengeName - the name of the challenge. + * @returns void + */ + setPassed(level, challengeName) { + const challenges = this.state[level]; + for (const challenge of challenges) { + if (challenge.name === challengeName) { + challenge.passed = true; + break; + } + } + + this._save(this.state); + } + + /** + * Merge the new state to the current state. this function will compare the new state with the current state and finally overwrite the current state based on the new state: + * - If the old key in the current state is not in the new state, the old key will be removed from the current state. + * - If the new key in the new state is not in the current state, the new key will be added to the current state. + * @param {object} newState + */ + _checkAndMerge(newState, oldState) { + if (!newState && !oldState) { + throw new Error('one of the new state and the old state is required.'); + } + + if (!newState) { + return oldState; + } + // TODO: compare the new state with the old state and merge the new state to the old state. + } } \ No newline at end of file From 1d0571b48b776f941c8d4be00d80422c5b89a12b Mon Sep 17 00:00:00 2001 From: 100gle Date: Tue, 26 Dec 2023 20:31:37 +0800 Subject: [PATCH 03/17] Update compare logic for old and new state --- static/js/passedState.js | 43 ++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/static/js/passedState.js b/static/js/passedState.js index 619c36a..1245788 100644 --- a/static/js/passedState.js +++ b/static/js/passedState.js @@ -3,7 +3,7 @@ class PassedState { _key = 'passedState'; - state = null; + _state = null; constructor(initialState = {}) { const currentState = localStorage.getItem(this._key); @@ -16,9 +16,9 @@ class PassedState { const rawState = this._prepareState(initialState); // check new state and old state whether is undefined or not. and merge the new state to the old state. - const state = this._checkAndMerge(rawState, currentState); + const state = this._checkAndMerge(currentState, rawState); this._save(state); - this.state = state; + this._state = state; return } @@ -48,7 +48,7 @@ class PassedState { } get() { - return this.state; + return this._state; } _save(state) { @@ -62,7 +62,7 @@ class PassedState { * @returns void */ setPassed(level, challengeName) { - const challenges = this.state[level]; + const challenges = this._state[level]; for (const challenge of challenges) { if (challenge.name === challengeName) { challenge.passed = true; @@ -70,7 +70,7 @@ class PassedState { } } - this._save(this.state); + this._save(this._state); } /** @@ -79,7 +79,7 @@ class PassedState { * - If the new key in the new state is not in the current state, the new key will be added to the current state. * @param {object} newState */ - _checkAndMerge(newState, oldState) { + _checkAndMerge(oldState, newState) { if (!newState && !oldState) { throw new Error('one of the new state and the old state is required.'); } @@ -87,6 +87,33 @@ class PassedState { if (!newState) { return oldState; } - // TODO: compare the new state with the old state and merge the new state to the old state. + + let mergedState = {}; + const levels = ['basic', 'intermediate', 'advanced', 'expert']; + + for (const level of levels) { + // Initialize an empty array for merged challenges + let mergedChallenges = []; + + // Create a map for quick lookup of challenges by name + const oldChallengesMap = new Map(oldState[level].map(challenge => [challenge.name, challenge])); + const newChallengesMap = new Map(newState[level].map(challenge => [challenge.name, challenge])); + + // Add or update challenges from the newState + for (const [name, newChallenge] of newChallengesMap.entries()) { + mergedChallenges.push({ ...newChallenge, passed: oldChallengesMap.get(name)?.passed }); + oldChallengesMap.delete(name); // Remove the challenge from oldChallengesMap since it's updated + } + + // Add remaining challenges from the oldState that are not updated (not present in newState) + for (const oldChallenge of oldChallengesMap.values()) { + mergedChallenges.push(oldChallenge); + } + + // Set the merged challenges for the current level in the mergedState + mergedState[level] = mergedChallenges; + } + + return mergedState; } } \ No newline at end of file From 9ed7e52e3ff8155fcd8287ce0caf0878943d5c4e Mon Sep 17 00:00:00 2001 From: 100gle Date: Tue, 26 Dec 2023 21:52:50 +0800 Subject: [PATCH 04/17] Fix state comparison bug --- static/js/passedState.js | 30 ++++++++++++--------- templates/components/challenge_sidebar.html | 5 ++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/static/js/passedState.js b/static/js/passedState.js index 1245788..86ff19d 100644 --- a/static/js/passedState.js +++ b/static/js/passedState.js @@ -5,7 +5,7 @@ class PassedState { _key = 'passedState'; _state = null; - constructor(initialState = {}) { + init(initialState) { const currentState = localStorage.getItem(this._key); // initialize the state when there is no state in the local storage. if (!currentState && !initialState) { @@ -16,12 +16,11 @@ class PassedState { const rawState = this._prepareState(initialState); // check new state and old state whether is undefined or not. and merge the new state to the old state. - const state = this._checkAndMerge(currentState, rawState); + const state = this._checkAndMerge(JSON.parse(currentState), rawState); this._save(state); this._state = state; return } - /** * prepare the state for initialization. * @param {object} rawState @@ -48,6 +47,10 @@ class PassedState { } get() { + if (!this._state) { + const currentState = localStorage.getItem(this._key); + this._state = JSON.parse(currentState); + } return this._state; } @@ -62,6 +65,10 @@ class PassedState { * @returns void */ setPassed(level, challengeName) { + if (!this._state) { + this.get() + } + const challenges = this._state[level]; for (const challenge of challenges) { if (challenge.name === challengeName) { @@ -84,12 +91,16 @@ class PassedState { throw new Error('one of the new state and the old state is required.'); } - if (!newState) { + if (!oldState && newState) { + return newState; + } + + if (!newState && oldState) { return oldState; } let mergedState = {}; - const levels = ['basic', 'intermediate', 'advanced', 'expert']; + const levels = ['basic', 'intermediate', 'advanced', 'extreme']; for (const level of levels) { // Initialize an empty array for merged challenges @@ -101,13 +112,8 @@ class PassedState { // Add or update challenges from the newState for (const [name, newChallenge] of newChallengesMap.entries()) { - mergedChallenges.push({ ...newChallenge, passed: oldChallengesMap.get(name)?.passed }); - oldChallengesMap.delete(name); // Remove the challenge from oldChallengesMap since it's updated - } - - // Add remaining challenges from the oldState that are not updated (not present in newState) - for (const oldChallenge of oldChallengesMap.values()) { - mergedChallenges.push(oldChallenge); + let hasPassed = oldChallengesMap.get(name)?.passed || newChallenge.passed; + mergedChallenges.push({ ...newChallenge, passed: hasPassed }); } // Set the merged challenges for the current level in the mergedState diff --git a/templates/components/challenge_sidebar.html b/templates/components/challenge_sidebar.html index 0886f2e..e3f3cc2 100644 --- a/templates/components/challenge_sidebar.html +++ b/templates/components/challenge_sidebar.html @@ -124,6 +124,7 @@
{{ level }}
+ From 2f357fc6f198d97198d438b5301acad5462f66fc Mon Sep 17 00:00:00 2001 From: 100gle Date: Tue, 26 Dec 2023 21:59:32 +0800 Subject: [PATCH 05/17] Sync data when challenge passed --- templates/components/challenge_area.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/components/challenge_area.html b/templates/components/challenge_area.html index df14218..7e79715 100644 --- a/templates/components/challenge_area.html +++ b/templates/components/challenge_area.html @@ -108,6 +108,8 @@ // add confetti effect when passed if (json.passed) { confetti.addConfetti() + // passedState has defined in challenge_sidebar.html + passedState.setPassed(level, name); } setTimeout(() => { document.getElementById('answer-link').style.display = 'block'; From da169fd8f0589d2a395b8352e05ab502e541239c Mon Sep 17 00:00:00 2001 From: 100gle Date: Wed, 27 Dec 2023 23:16:11 +0800 Subject: [PATCH 06/17] Add finishment for challenges --- templates/components/challenge_area.html | 5 +-- templates/components/challenge_sidebar.html | 36 +++++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/templates/components/challenge_area.html b/templates/components/challenge_area.html index 7e79715..42ad34d 100644 --- a/templates/components/challenge_area.html +++ b/templates/components/challenge_area.html @@ -110,6 +110,7 @@ confetti.addConfetti() // passedState has defined in challenge_sidebar.html passedState.setPassed(level, name); + document.getElementById(`${level}-${name}`).parentNode.classList.add('passed'); } setTimeout(() => { document.getElementById('answer-link').style.display = 'block'; @@ -153,8 +154,8 @@ } // Make sure the current challenge is visible to user. - activeChallengeInList = document.getElementById(`${level}-${name}`); - activeChallengeInList.classList.add('active-challenge'); // Highlight + let activeChallengeInList = document.getElementById(`${level}-${name}`); + activeChallengeInList.parentNode.classList.add('active-challenge'); // Highlight } codeUnderTest = {{code_under_test | tojson}}; diff --git a/templates/components/challenge_sidebar.html b/templates/components/challenge_sidebar.html index e3f3cc2..25d9ca7 100644 --- a/templates/components/challenge_sidebar.html +++ b/templates/components/challenge_sidebar.html @@ -10,14 +10,6 @@ flex-direction: column; } - .challenge-nav ul li { - padding: 0px 8px; - } - - .challenge-nav ul li:not(:first-of-type) a { - padding: 8px 10px; - } - .challenge-nav .challenge-level { width: fit-content; border-bottom: 1px dashed hsl(240, 2%, 90%); @@ -36,6 +28,22 @@ display: none; } + .passed { + position: relative; + } + + .passed::after { + /* iconify: https://icon-sets.iconify.design/lets-icons/done-ring-round */ + content: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="24" height="24" viewBox="0 0 24 24"%3E%3Cg fill="none" stroke="%2366ba6f" stroke-linecap="round" stroke-width="2"%3E%3Cpath d="m9 10l3.258 2.444a1 1 0 0 0 1.353-.142L20 5"%2F%3E%3Cpath d="M21 12a9 9 0 1 1-6.67-8.693"%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E'); + display: inline-block; + position: absolute; + width: 24px; + height: 24px; + top: 50%; + right: 0; + transform: translate(-50%, -50%); + transition: all 1.5s ease-out; + } @media only screen and (max-width: 800px) { .sidebar-toggle { @@ -141,7 +149,7 @@
{{ level }}
* @param {Event} event - The click event. */ function removeHighlight(event) { - previousActiveChallenges = document.getElementsByClassName("active-challenge"); + let previousActiveChallenges = document.getElementsByClassName("active-challenge"); for (c of previousActiveChallenges) { // Remove previously highlighted challenge in the list. c.classList.remove('active-challenge'); @@ -151,4 +159,14 @@
{{ level }}
const initialState = {{ challenges_groupby_level | tojson }}; const passedState = new PassedState(); passedState.init(initialState); + + // Highlight the passed challenges when the page is loaded. + let ids = Object.keys(passedState.get()).forEach(level => { + passedState.get()[level].forEach(challenge => { + let id = `#${level}-${challenge.name}`; + if (challenge.passed) { + document.querySelector(id).parentNode.classList.add('passed'); + } + }) + }) From db1198a82b033acbebb59aac6701a0c5101947dc Mon Sep 17 00:00:00 2001 From: 100gle Date: Thu, 28 Dec 2023 19:50:13 +0800 Subject: [PATCH 07/17] Rename the custom js file --- static/js/{passedState.js => passed-state.js} | 2 +- templates/components/challenge_sidebar.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename static/js/{passedState.js => passed-state.js} (99%) diff --git a/static/js/passedState.js b/static/js/passed-state.js similarity index 99% rename from static/js/passedState.js rename to static/js/passed-state.js index 86ff19d..98926d1 100644 --- a/static/js/passedState.js +++ b/static/js/passed-state.js @@ -2,7 +2,7 @@ class PassedState { - _key = 'passedState'; + _key = 'python-type-challenges'; _state = null; init(initialState) { diff --git a/templates/components/challenge_sidebar.html b/templates/components/challenge_sidebar.html index 25d9ca7..1b2c924 100644 --- a/templates/components/challenge_sidebar.html +++ b/templates/components/challenge_sidebar.html @@ -132,7 +132,7 @@
{{ level }}
- + diff --git a/templates/components/challenge_sidebar.html b/templates/components/challenge_sidebar.html index 60abffd..90daa4b 100644 --- a/templates/components/challenge_sidebar.html +++ b/templates/components/challenge_sidebar.html @@ -140,8 +140,8 @@
{{ level }}
- - + \ No newline at end of file diff --git a/templates/components/challenge_sidebar.html b/templates/components/challenge_sidebar.html index dcf120c..fa50ef5 100644 --- a/templates/components/challenge_sidebar.html +++ b/templates/components/challenge_sidebar.html @@ -142,6 +142,22 @@
{{ level }}
+ +