diff --git a/src/api/admin/hintsAPI.js b/src/api/admin/hintsAPI.js new file mode 100644 index 0000000..cd969d5 --- /dev/null +++ b/src/api/admin/hintsAPI.js @@ -0,0 +1,17 @@ +import axiosInstance from "../axiosInstance.js"; + +export default { + async getHintStatus(hintId) { + return await axiosInstance({ + method: "get", + url: `/api/info/hint/${hintId}` + }); + }, + + async takeHint(hintId) { + return await axiosInstance({ + method: "post", + url: `/api/info/hint/${hintId}` + }); + } +}; diff --git a/src/api/axiosInstance.js b/src/api/axiosInstance.js index 7b6609d..2f4362b 100644 --- a/src/api/axiosInstance.js +++ b/src/api/axiosInstance.js @@ -32,6 +32,12 @@ axiosInstance.interceptors.response.use( "/auth/register", "/api/submit/challenge" ]; + + // For hint errors, pass through the error response + if (error.response && error.response.config.url.includes("/api/info/hint")) { + return Promise.reject(error); + } + if (!error.response) { router.push("/error/networkerror"); } else if (ignoreErrorPagesPath.includes(error.response.config.url)) { @@ -49,7 +55,6 @@ axiosInstance.interceptors.response.use( default: return error; } - Promise.reject(error); } } diff --git a/src/components/ChallCard.vue b/src/components/ChallCard.vue index 488c671..2432cac 100644 --- a/src/components/ChallCard.vue +++ b/src/components/ChallCard.vue @@ -16,6 +16,9 @@ | {{ challDetails.solves.length }} Solves + + | {{ challDetails.previous_tries }}/{{ challDetails.maxAttemptLimit }} attempts +
@@ -48,7 +51,7 @@ @@ -76,12 +79,61 @@
+ +
+ +
+
+ + + + +
+
+ Maximum attempts reached +
diff --git a/src/components/LeaderboardGraph.vue b/src/components/LeaderboardGraph.vue index 18dc32a..3a66029 100644 --- a/src/components/LeaderboardGraph.vue +++ b/src/components/LeaderboardGraph.vue @@ -3,7 +3,7 @@ :chartData="this.lineGraphData()" :options="this.lineGraphOptions" class="lineGraph" - :height="150" + :height="250" v-if="this.users.length > 0 && this.scoreSeries.length > 0" /> diff --git a/src/components/Stats.vue b/src/components/Stats.vue index f15e2b6..2f8ee7b 100644 --- a/src/components/Stats.vue +++ b/src/components/Stats.vue @@ -9,11 +9,11 @@

Total

-

{{ details.challenges.length }}

+

{{ getSolvedCount }}

Solved

-

{{ total - details.challenges.length }}

+

{{ total - getSolvedCount }}

Unsolved

@@ -23,6 +23,22 @@ diff --git a/src/config/config.js b/src/config/config.js index 8bc98f4..f6be335 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -1,6 +1,6 @@ export const CONFIG = { beastRoot: "http://localhost:5005", - staticRoot: "http://static.beast.sdslabs.local/static/", + staticRoot: "http://localhost:5005/", webRoot: "http://hack.beast.sdslabs.local", ncRoot: "hack.beast.sdslabs.local" }; diff --git a/src/styles/modules/_challCard.scss b/src/styles/modules/_challCard.scss index 257e37c..90a3b03 100644 --- a/src/styles/modules/_challCard.scss +++ b/src/styles/modules/_challCard.scss @@ -79,6 +79,11 @@ line-height: 1.125rem; align-items: center; color: $theme-color-grey39; + + .attempts-counter { + color: $theme-color-grey57; + font-size: 0.875rem; + } } .challCard-challDesc { @@ -275,6 +280,61 @@ } } } + + .challCard-submit { + margin-top: 1rem; + padding: 1rem; + border-top: 1px solid #e0e0e0; + + &-container { + display: flex; + gap: 1rem; + } + + &-input { + flex: 1; + padding: 0.5rem 1rem; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-family: $primary-font-family; + font-size: 0.875rem; + + &:focus { + outline: none; + border-color: #007bff; + } + } + + &-button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + background-color: #007bff; + color: white; + font-family: $primary-font-family; + font-size: 0.875rem; + cursor: pointer; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:not(:disabled):hover { + background-color: #0056b3; + } + } + } + + .challCard-maxed { + text-align: center; + color: #dc3545; + font-family: $primary-font-family; + font-size: 0.875rem; + margin-top: 1rem; + padding: 1rem; + border-top: 1px solid #e0e0e0; + } } .preview-challcard { @@ -294,3 +354,148 @@ letter-spacing: 0.1rem; cursor: pointer; } + +.challCard-hints { + margin: 20px 0; + padding: 16px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + + .hint-buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 12px; + } + + .hint-button { + flex: 1; + min-width: 200px; + padding: 12px 16px; + border-radius: 8px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background: #2c3e50; + color: white; + + &.hint-taken { + background: linear-gradient(135deg, #27ae60, #2ecc71); + } + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + opacity: 0.9; + } + + &.hint-loading { + background-color: #95a5a6; + cursor: not-allowed; + opacity: 0.7; + } + + .hint-button-content { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .hint-number { + font-weight: 600; + font-size: 15px; + } + + .hint-points { + font-size: 14px; + opacity: 0.8; + } + } +} + +// Modal styles +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.modal-content { + background: white; + padding: 24px; + border-radius: 8px; + max-width: 400px; + width: 90%; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + position: relative; + z-index: 10000; + + h3 { + color: #2c3e50; + margin: 0 0 16px 0; + font-size: 20px; + font-weight: 600; + } + + p { + color: #4a5568; + margin: 0 0 24px 0; + font-size: 16px; + line-height: 1.5; + + &.hint-text { + background: #f8f9fa; + padding: 16px; + border-radius: 6px; + border: 1px solid #e9ecef; + font-family: $primary-font-family; + white-space: pre-wrap; + } + } + + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + + button { + min-width: 100px; + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + + &.modal-cancel { + background: #e2e8f0; + color: #4a5568; + border: none; + + &:hover { + background: #cbd5e0; + } + } + + &.modal-confirm { + background: $theme-color-dark-orange; + color: white; + border: none; + + &:hover { + opacity: 0.9; + } + } + } + } +} diff --git a/src/utils/challenges.js b/src/utils/challenges.js index bbc71d6..fb94d48 100644 --- a/src/utils/challenges.js +++ b/src/utils/challenges.js @@ -8,11 +8,8 @@ export const getChallenges = async (getUserSolves, username) => { if (getUserSolves) { let userData = await UserService.getUserByUsername(username); challenges.forEach(challenge => { - if ( - userData.data.challenges.find(el => { - return el.id === challenge.id; - }) - ) { + // Check if the current user has solved this challenge + if (challenge.solves && challenge.solves.some(solve => solve.username === username)) { challenge.isSolved = true; } }); diff --git a/src/views/AdminChallenge.vue b/src/views/AdminChallenge.vue index eb0efca..596b855 100644 --- a/src/views/AdminChallenge.vue +++ b/src/views/AdminChallenge.vue @@ -99,7 +99,7 @@
@@ -246,9 +246,9 @@ export default { } return `${CONFIG.webRoot}:${port}`; }, - getStaticUrl(asset) { + getStaticUrl(name, asset) { let url = CONFIG.staticRoot; - return `${url}${asset}`; + return `${url}api/info/download?challenge=${name}&asset=${asset}`; }, getFileFromAsset(asset) { let paths = asset.split("/"); diff --git a/src/views/Challenges.vue b/src/views/Challenges.vue index 4e137e5..d4885a8 100644 --- a/src/views/Challenges.vue +++ b/src/views/Challenges.vue @@ -12,7 +12,11 @@
- +
-
@@ -55,13 +55,11 @@ import moment from "moment-timezone"; import LoginUser from "../api/admin/authAPI.js"; export default { name: "home", - components: { - Button - }, + props: ["fetchedData", "configs"], data() { return { - configData: {} + configData: {}, }; }, beforeCreate() { @@ -69,7 +67,7 @@ export default { }, mounted() { if (!this.fetchedData) { - ConfigApiService.getConfigs().then(response => { + ConfigApiService.getConfigs().then((response) => { this.configData = response.data; }); } else { @@ -88,8 +86,8 @@ export default { return moment(time, "HH:mm:ss UTC: Z, DD MMMM YYYY, dddd").format( "HH:mm:ss UTC: Z, Do MMMM YYYY, dddd" ); - } - } + }, + }, }; diff --git a/src/views/Register.vue b/src/views/Register.vue index e697afd..480773e 100644 --- a/src/views/Register.vue +++ b/src/views/Register.vue @@ -100,6 +100,7 @@ confirmPassword && email && !PassErr && + !PassLen && !UsernameErr && !EmailErr && password === confirmPassword &&