new: [app] Added various improvements such as admin panel, enabled exercises, ...
This commit is contained in:
parent
c9d90283eb
commit
efa4d7613b
13 changed files with 284 additions and 45 deletions
3
db.py
3
db.py
|
@ -5,7 +5,8 @@ import collections
|
||||||
|
|
||||||
USER_ID_TO_EMAIL_MAPPING = {}
|
USER_ID_TO_EMAIL_MAPPING = {}
|
||||||
USER_ID_TO_AUTHKEY_MAPPING = {}
|
USER_ID_TO_AUTHKEY_MAPPING = {}
|
||||||
ALL_EXERCICES = []
|
ALL_EXERCISES = []
|
||||||
|
SELECTED_EXERCISES = []
|
||||||
INJECT_BY_UUID = {}
|
INJECT_BY_UUID = {}
|
||||||
INJECT_SEQUENCE_BY_INJECT_UUID = {}
|
INJECT_SEQUENCE_BY_INJECT_UUID = {}
|
||||||
INJECT_REQUIREMENTS_BY_INJECT_UUID = {}
|
INJECT_REQUIREMENTS_BY_INJECT_UUID = {}
|
||||||
|
|
43
exercise.py
43
exercise.py
|
@ -14,11 +14,9 @@ ACTIVE_EXERCISES_DIR = "active_exercises"
|
||||||
|
|
||||||
|
|
||||||
def load_exercises() -> bool:
|
def load_exercises() -> bool:
|
||||||
db.ALL_EXERCICES = read_exercise_dir()
|
db.ALL_EXERCISES = read_exercise_dir()
|
||||||
init_inject_flow()
|
init_inject_flow()
|
||||||
init_exercises_tasks()
|
init_exercises_tasks()
|
||||||
# mark_task_completed(10, "4703a4b2-0ae4-47f3-9dc3-91250be60156", "e2216993-6192-4e7c-ae30-97cfe9de61b4") # filtering - past 48h
|
|
||||||
# mark_task_completed(10, "29324587-db6c-4a73-a209-cf8c79871629", "a6b5cf88-ba93-4c3f-8265-04e00d53778e") # Data - Event creation
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,12 +33,12 @@ def read_exercise_dir():
|
||||||
|
|
||||||
|
|
||||||
def init_inject_flow():
|
def init_inject_flow():
|
||||||
for exercise in db.ALL_EXERCICES:
|
for exercise in db.ALL_EXERCISES:
|
||||||
for inject in exercise['injects']:
|
for inject in exercise['injects']:
|
||||||
inject['exercise_uuid'] = exercise['exercise']['uuid']
|
inject['exercise_uuid'] = exercise['exercise']['uuid']
|
||||||
db.INJECT_BY_UUID[inject['uuid']] = inject
|
db.INJECT_BY_UUID[inject['uuid']] = inject
|
||||||
|
|
||||||
for exercise in db.ALL_EXERCICES:
|
for exercise in db.ALL_EXERCISES:
|
||||||
for inject_flow in exercise['inject_flow']:
|
for inject_flow in exercise['inject_flow']:
|
||||||
db.INJECT_REQUIREMENTS_BY_INJECT_UUID[inject_flow['inject_uuid']] = inject_flow['requirements']
|
db.INJECT_REQUIREMENTS_BY_INJECT_UUID[inject_flow['inject_uuid']] = inject_flow['requirements']
|
||||||
db.INJECT_SEQUENCE_BY_INJECT_UUID[inject_flow['inject_uuid']] = []
|
db.INJECT_SEQUENCE_BY_INJECT_UUID[inject_flow['inject_uuid']] = []
|
||||||
|
@ -49,7 +47,7 @@ def init_inject_flow():
|
||||||
|
|
||||||
|
|
||||||
def init_exercises_tasks():
|
def init_exercises_tasks():
|
||||||
for exercise in db.ALL_EXERCICES:
|
for exercise in db.ALL_EXERCISES:
|
||||||
tasks = {}
|
tasks = {}
|
||||||
for inject in exercise['injects']:
|
for inject in exercise['injects']:
|
||||||
tasks[inject['uuid']] = {
|
tasks[inject['uuid']] = {
|
||||||
|
@ -65,8 +63,8 @@ def init_exercises_tasks():
|
||||||
|
|
||||||
|
|
||||||
def get_exercises():
|
def get_exercises():
|
||||||
exercices = []
|
exercises = []
|
||||||
for exercise in db.ALL_EXERCICES:
|
for exercise in db.ALL_EXERCISES:
|
||||||
tasks = []
|
tasks = []
|
||||||
for inject in exercise['injects']:
|
for inject in exercise['injects']:
|
||||||
score = 0
|
score = 0
|
||||||
|
@ -82,7 +80,7 @@ def get_exercises():
|
||||||
"score": score,
|
"score": score,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
exercices.append(
|
exercises.append(
|
||||||
{
|
{
|
||||||
"name": exercise['exercise']['name'],
|
"name": exercise['exercise']['name'],
|
||||||
"uuid": exercise['exercise']['uuid'],
|
"uuid": exercise['exercise']['uuid'],
|
||||||
|
@ -92,8 +90,28 @@ def get_exercises():
|
||||||
"tasks": tasks,
|
"tasks": tasks,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
exercices = sorted(exercices, key=lambda d: d['priority'])
|
exercises = sorted(exercises, key=lambda d: d['priority'])
|
||||||
return exercices
|
return exercises
|
||||||
|
|
||||||
|
|
||||||
|
def get_selected_exercises():
|
||||||
|
return db.SELECTED_EXERCISES
|
||||||
|
|
||||||
|
|
||||||
|
def change_exercise_selection(exercise_uuid: str, selected: bool):
|
||||||
|
if selected:
|
||||||
|
if exercise_uuid not in db.SELECTED_EXERCISES:
|
||||||
|
db.SELECTED_EXERCISES.append(exercise_uuid)
|
||||||
|
else:
|
||||||
|
if exercise_uuid in db.SELECTED_EXERCISES:
|
||||||
|
db.SELECTED_EXERCISES.remove(exercise_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def resetAllExerciseProgress():
|
||||||
|
for user_id in db.USER_ID_TO_EMAIL_MAPPING.keys():
|
||||||
|
for exercise_status in db.EXERCISES_STATUS.values():
|
||||||
|
for task in exercise_status['tasks'].values():
|
||||||
|
mark_task_incomplete(user_id, exercise_status['uuid'], task['uuid'])
|
||||||
|
|
||||||
|
|
||||||
def get_completed_tasks_for_user(user_id: int):
|
def get_completed_tasks_for_user(user_id: int):
|
||||||
|
@ -338,6 +356,9 @@ def check_active_tasks(user_id: int, data: dict, context: dict) -> bool:
|
||||||
available_tasks = get_available_tasks_for_user(user_id)
|
available_tasks = get_available_tasks_for_user(user_id)
|
||||||
for task_uuid in available_tasks:
|
for task_uuid in available_tasks:
|
||||||
inject = db.INJECT_BY_UUID[task_uuid]
|
inject = db.INJECT_BY_UUID[task_uuid]
|
||||||
|
if inject['exercise_uuid'] not in db.SELECTED_EXERCISES:
|
||||||
|
print(f'exercise not active for this inject {inject['name']}')
|
||||||
|
continue
|
||||||
print(f'checking: {inject['name']}')
|
print(f'checking: {inject['name']}')
|
||||||
completed = check_inject(user_id, inject, data, context)
|
completed = check_inject(user_id, inject, data, context)
|
||||||
if completed:
|
if completed:
|
||||||
|
|
45
package-lock.json
generated
45
package-lock.json
generated
|
@ -20,6 +20,7 @@
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"daisyui": "^4.12.10",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
@ -1419,6 +1420,16 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-selector-tokenizer": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"fastparse": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
@ -1436,6 +1447,34 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/culori": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/daisyui": {
|
||||||
|
"version": "4.12.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz",
|
||||||
|
"integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-selector-tokenizer": "^0.8",
|
||||||
|
"culori": "^3",
|
||||||
|
"picocolors": "^1",
|
||||||
|
"postcss-js": "^4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/daisyui"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||||
|
@ -1856,6 +1895,12 @@
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fastparse": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"daisyui": "^4.12.10",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
|
16
server.py
16
server.py
|
@ -40,6 +40,14 @@ def disconnect(sid):
|
||||||
def get_exercises(sid):
|
def get_exercises(sid):
|
||||||
return exercise_model.get_exercises()
|
return exercise_model.get_exercises()
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
def get_selected_exercises(sid):
|
||||||
|
return exercise_model.get_selected_exercises()
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
def change_exercise_selection(sid, payload):
|
||||||
|
return exercise_model.change_exercise_selection(payload['exercise_uuid'], payload['selected'])
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def get_progress(sid):
|
def get_progress(sid):
|
||||||
return exercise_model.get_progress()
|
return exercise_model.get_progress()
|
||||||
|
@ -50,11 +58,15 @@ def get_notifications(sid):
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def mark_task_completed(sid, payload):
|
def mark_task_completed(sid, payload):
|
||||||
return exercise_model.mark_task_completed(payload['user_id'], payload['exercise_uuid'], payload['task_uuid'])
|
return exercise_model.mark_task_completed(int(payload['user_id']), payload['exercise_uuid'], payload['task_uuid'])
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def mark_task_incomplete(sid, payload):
|
def mark_task_incomplete(sid, payload):
|
||||||
return exercise_model.mark_task_incomplete(payload['user_id'], payload['exercise_uuid'], payload['task_uuid'])
|
return exercise_model.mark_task_incomplete(int(payload['user_id']), payload['exercise_uuid'], payload['task_uuid'])
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
def reset_all_exercise_progress(sid):
|
||||||
|
return exercise_model.resetAllExerciseProgress()
|
||||||
|
|
||||||
@sio.on('*')
|
@sio.on('*')
|
||||||
def any_event(event, sid, data={}):
|
def any_event(event, sid, data={}):
|
||||||
|
|
10
src/App.vue
10
src/App.vue
|
@ -1,8 +1,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import TheDahboard from './TheDahboard.vue'
|
|
||||||
import TheThemeButton from './components/TheThemeButton.vue'
|
import TheThemeButton from './components/TheThemeButton.vue'
|
||||||
|
import TheAdminPanel from './components/TheAdminPanel.vue'
|
||||||
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
|
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
|
||||||
|
import TheDahboard from './TheDahboard.vue'
|
||||||
import { socketConnected } from "@/socket";
|
import { socketConnected } from "@/socket";
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,8 +17,11 @@ onMounted(() => {
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<div class="absolute top-1 right-1">
|
<div class="absolute top-1 right-1">
|
||||||
<TheThemeButton></TheThemeButton>
|
<div class="flex gap-2">
|
||||||
<TheSocketConnectionState></TheSocketConnectionState>
|
<TheThemeButton></TheThemeButton>
|
||||||
|
<TheAdminPanel></TheAdminPanel>
|
||||||
|
<TheSocketConnectionState></TheSocketConnectionState>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TheDahboard></TheDahboard>
|
<TheDahboard></TheDahboard>
|
||||||
</main>
|
</main>
|
||||||
|
|
71
src/components/TheAdminPanel.vue
Normal file
71
src/components/TheAdminPanel.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { exercises, selected_exercises, resetAllExerciseProgress, changeExerciseSelection } from "@/socket";
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { faScrewdriverWrench, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
const admin_modal = ref(null)
|
||||||
|
|
||||||
|
function changeSelectionState(state_enabled, exec_uuid) {
|
||||||
|
changeExerciseSelection(exec_uuid, state_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="admin_modal.showModal()"
|
||||||
|
class="px-2 py-1 rounded-md focus-outline font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
|
||||||
|
Admin panel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog ref="admin_modal" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-6xl top-24 absolute bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200">
|
||||||
|
<h2 class="text-2xl font-bold">
|
||||||
|
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
|
||||||
|
Admin panel
|
||||||
|
</h2>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="flex mb-5">
|
||||||
|
<button
|
||||||
|
@click="resetAllExerciseProgress()"
|
||||||
|
class="px-2 py-1 rounded-md focus-outline font-semibold bg-red-600 text-slate-200 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faTrash" class="mr-1"></FontAwesomeIcon>
|
||||||
|
Reset All Exercises
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold">Selected Exercises</h3>
|
||||||
|
<div
|
||||||
|
v-for="(exercise) in exercises"
|
||||||
|
:key="exercise.name"
|
||||||
|
class="form-control pl-3"
|
||||||
|
>
|
||||||
|
<label class="label cursor-pointer justify-start">
|
||||||
|
<input
|
||||||
|
@change="changeSelectionState($event.target.checked, exercise.uuid)"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="selected_exercises.includes(exercise.uuid)"
|
||||||
|
:value="exercise.uuid"
|
||||||
|
:class="`checkbox ${selected_exercises.includes(exercise.uuid) ? 'checkbox-success' : ''}`"
|
||||||
|
/>
|
||||||
|
<span class="font-mono font-semibold text-base ml-3">{{ exercise.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
|
@ -57,7 +57,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="Object.keys(notifications).length == 0">
|
<tr v-if="notifications.length == 0">
|
||||||
<td
|
<td
|
||||||
colspan="5"
|
colspan="5"
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
||||||
|
@ -89,6 +89,10 @@
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
||||||
>POST</span>
|
>POST</span>
|
||||||
|
<span v-else-if="notification.http_method == 'PUT'"
|
||||||
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
|
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
||||||
|
>PUT</span>
|
||||||
<span v-else-if="notification.http_method == 'DELETE'"
|
<span v-else-if="notification.http_method == 'DELETE'"
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
dark:bg-red-600 dark:text-neutral-100 bg-red-600 text-neutral-100"
|
dark:bg-red-600 dark:text-neutral-100 bg-red-600 text-neutral-100"
|
||||||
|
|
|
@ -1,23 +1,17 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { exercises, progresses } from "@/socket";
|
import { computed } from "vue";
|
||||||
|
import { active_exercises as exercises, progresses, setCompletedState } from "@/socket";
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { faCheck, faTimes, faGraduationCap } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck, faTimes, faGraduationCap } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
|
||||||
function toggle_completed(completed, user_id, exec_uuid, task_uuid) {
|
function toggleCompleted(completed, user_id, exec_uuid, task_uuid) {
|
||||||
const payload = {
|
setCompletedState(completed, user_id, exec_uuid, task_uuid)
|
||||||
user_id: user_id,
|
|
||||||
exercise_uuid: exec_uuid,
|
|
||||||
task_uuid: task_uuid,
|
|
||||||
}
|
|
||||||
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
|
|
||||||
socket.emit(event_name, payload, () => {
|
|
||||||
socket.emit("get_progress", (all_progress) => {
|
|
||||||
socketState.progresses = all_progress
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasExercises = computed(() => exercises.value.length > 0)
|
||||||
|
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -25,6 +19,13 @@
|
||||||
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
||||||
Active Exercises
|
Active Exercises
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!hasExercises"
|
||||||
|
class="text-center text-slate-600 dark:text-slate-400 p-3 pl-6"
|
||||||
|
>
|
||||||
|
<i>- No Exercise available -</i>
|
||||||
|
</div>
|
||||||
<table
|
<table
|
||||||
v-for="(exercise, exercise_index) in exercises"
|
v-for="(exercise, exercise_index) in exercises"
|
||||||
:key="exercise.name"
|
:key="exercise.name"
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="Object.keys(progresses).length == 0">
|
<tr v-if="!hasProgress">
|
||||||
<td
|
<td
|
||||||
:colspan="2 + exercise.tasks.length"
|
:colspan="2 + exercise.tasks.length"
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
||||||
|
@ -85,8 +86,8 @@
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3"
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="select-none cursor-pointer"
|
class="select-none cursor-pointer text-nowrap"
|
||||||
@click="toggle_completed(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
|
@click="toggleCompleted(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? faCheck : faTimes"
|
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? faCheck : faTimes"
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { socketConnected } from "@/socket";
|
import { socketConnected } from "@/socket";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,12 +15,41 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<div class="flex">
|
||||||
@click="darkMode = !darkMode"
|
<label class="grid cursor-pointer place-items-center">
|
||||||
class="mr-3 px-2 py-1 rounded-md focus-outline font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700"
|
<input
|
||||||
>
|
type="checkbox"
|
||||||
<FontAwesomeIcon :icon="faSun" class="mr-1" v-show="!darkMode"></FontAwesomeIcon>
|
@click="darkMode = !darkMode"
|
||||||
<FontAwesomeIcon :icon="faMoon" class="mr-1" v-show="darkMode"></FontAwesomeIcon>
|
:checked="darkMode"
|
||||||
{{ darkMode ? 'Dark' : 'Light'}}
|
class="toggle theme-controller bg-slate-400 col-span-2 col-start-1 row-start-1 [--tglbg:#e2e8f0]" />
|
||||||
</button>
|
<svg
|
||||||
|
class="stroke-base-100 fill-base-100 col-start-1 row-start-1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<path
|
||||||
|
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="stroke-base-100 fill-base-100 col-start-2 row-start-1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
|
@ -10,6 +10,7 @@ const initial_state = {
|
||||||
notificationCounter: 0,
|
notificationCounter: 0,
|
||||||
notificationAPICounter: 0,
|
notificationAPICounter: 0,
|
||||||
exercises: [],
|
exercises: [],
|
||||||
|
selected_exercises: [],
|
||||||
progresses: {},
|
progresses: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +20,8 @@ const connectionState = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const exercises = computed(() => state.exercises)
|
export const exercises = computed(() => state.exercises)
|
||||||
|
export const selected_exercises = computed(() => state.selected_exercises)
|
||||||
|
export const active_exercises = computed(() => state.exercises.filter((exercise) => state.selected_exercises.includes(exercise.uuid)))
|
||||||
export const progresses = computed(() => state.progresses)
|
export const progresses = computed(() => state.progresses)
|
||||||
export const notifications = computed(() => state.notificationEvents)
|
export const notifications = computed(() => state.notificationEvents)
|
||||||
export const notificationCounter = computed(() => state.notificationCounter)
|
export const notificationCounter = computed(() => state.notificationCounter)
|
||||||
|
@ -34,6 +37,9 @@ export function fullReload() {
|
||||||
socket.emit("get_exercises", (all_exercises) => {
|
socket.emit("get_exercises", (all_exercises) => {
|
||||||
state.exercises = all_exercises
|
state.exercises = all_exercises
|
||||||
})
|
})
|
||||||
|
socket.emit("get_selected_exercises", (all_selected_exercises) => {
|
||||||
|
state.selected_exercises = all_selected_exercises
|
||||||
|
})
|
||||||
socket.emit("get_notifications", (all_notifications) => {
|
socket.emit("get_notifications", (all_notifications) => {
|
||||||
state.notificationEvents = all_notifications
|
state.notificationEvents = all_notifications
|
||||||
})
|
})
|
||||||
|
@ -42,6 +48,40 @@ export function fullReload() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCompletedState(completed, user_id, exec_uuid, task_uuid) {
|
||||||
|
const payload = {
|
||||||
|
user_id: user_id,
|
||||||
|
exercise_uuid: exec_uuid,
|
||||||
|
task_uuid: task_uuid,
|
||||||
|
}
|
||||||
|
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
|
||||||
|
socket.emit(event_name, payload, () => {
|
||||||
|
socket.emit("get_progress", (all_progress) => {
|
||||||
|
state.progresses = all_progress
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetAllExerciseProgress() {
|
||||||
|
socket.emit("reset_all_exercise_progress", () => {
|
||||||
|
socket.emit("get_progress", (all_progress) => {
|
||||||
|
state.progresses = all_progress
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeExerciseSelection(exec_uuid, state_enabled) {
|
||||||
|
const payload = {
|
||||||
|
exercise_uuid: exec_uuid,
|
||||||
|
selected: state_enabled,
|
||||||
|
}
|
||||||
|
socket.emit("change_exercise_selection", payload, () => {
|
||||||
|
socket.emit("get_selected_exercises", (all_selected_exercises) => {
|
||||||
|
state.selected_exercises = all_selected_exercises
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const socket = io(URL, {
|
const socket = io(URL, {
|
||||||
autoConnect: true
|
autoConnect: true
|
||||||
|
|
|
@ -11,6 +11,18 @@ export default {
|
||||||
} ,
|
} ,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('daisyui'),
|
||||||
|
],
|
||||||
darkMode: ['selector'],
|
darkMode: ['selector'],
|
||||||
|
daisyui: {
|
||||||
|
themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
|
||||||
|
darkTheme: "dark", // name of one of the included themes for dark mode
|
||||||
|
base: false, // applies background color and foreground color for root element by default
|
||||||
|
styled: true, // include daisyUI colors and design decisions for all components
|
||||||
|
utils: false, // adds responsive and modifier utility classes
|
||||||
|
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
|
||||||
|
logs: false, // Shows info about daisyUI version and used config in the console when building your CSS
|
||||||
|
themeRoot: ":root", // The element that receives theme color CSS variables
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue