diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index bb4d37fd1a49afc92c9df6f535c4956fdebb805b..99bebaa8f4285653b160bd34a44af2eccc791ae5 100644 Binary files a/backend/secfit/requirements.txt and b/backend/secfit/requirements.txt differ diff --git a/backend/secfit/workouts/admin.py b/backend/secfit/workouts/admin.py index cb43794b85492adcb933dc4e46f875029dc411cf..777980c0f343933b46363a7fb467c960ea514d25 100644 --- a/backend/secfit/workouts/admin.py +++ b/backend/secfit/workouts/admin.py @@ -9,3 +9,4 @@ admin.site.register(Exercise) admin.site.register(ExerciseInstance) admin.site.register(Workout) admin.site.register(WorkoutFile) + diff --git a/backend/secfit/workouts/migrations/0004_workout_planned.py b/backend/secfit/workouts/migrations/0004_workout_planned.py new file mode 100644 index 0000000000000000000000000000000000000000..caccf27985e04eb3cfb1ef65cba921359a4c5c0f --- /dev/null +++ b/backend/secfit/workouts/migrations/0004_workout_planned.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-02-27 12:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workouts', '0003_rememberme'), + ] + + operations = [ + migrations.AddField( + model_name='workout', + name='planned', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 5e3c6d1614d54992b42491bee8207a65410c5961..108cb597a8a35ca00295e97116699c53ecabd364 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -38,6 +38,7 @@ class Workout(models.Model): notes: Notes about the workout owner: User that logged the workout visibility: The visibility level of the workout: Public, Coach, or Private + planned: Indicates if it is a planned workout """ name = models.CharField(max_length=100) @@ -46,6 +47,7 @@ class Workout(models.Model): owner = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, related_name="workouts" ) + planned = models.BooleanField(default=False) # Visibility levels PUBLIC = "PU" # Visible to all authenticated users @@ -67,7 +69,6 @@ class Workout(models.Model): def __str__(self): return self.name - class Exercise(models.Model): """Django model for an exercise type that users can create. diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index a966ed3d752dcf54767a10f2b4b53d416e095a33..b36de6aefd2025a81fda2bfb1b32edf1544090a1 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -3,6 +3,8 @@ from rest_framework import serializers from rest_framework.serializers import HyperlinkedRelatedField from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe +from datetime import datetime +import pytz class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): @@ -52,7 +54,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): This serializer specifies nested serialization since a workout consists of WorkoutFiles and ExerciseInstances. - Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity, + Serialized fields: url, id, name, date, notes, owner, planned, owner_username, visiblity, exercise_instances, files Attributes: @@ -74,6 +76,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): "date", "notes", "owner", + "planned", "owner_username", "visibility", "exercise_instances", @@ -93,6 +96,19 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): Returns: Workout: A newly created Workout """ + # Check if date is valid + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + + if validated_data["planned"]: + if timeNowAdjusted >= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be a future date"]}) + else: + if timeNowAdjusted <= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be an old date"]}) + exercise_instances_data = validated_data.pop("exercise_instances") files_data = [] if "files" in validated_data: @@ -101,10 +117,12 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): workout = Workout.objects.create(**validated_data) for exercise_instance_data in exercise_instances_data: - ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) + ExerciseInstance.objects.create( + workout=workout, **exercise_instance_data) for file_data in files_data: WorkoutFile.objects.create( - workout=workout, owner=workout.owner, file=file_data.get("file") + workout=workout, owner=workout.owner, file=file_data.get( + "file") ) return workout @@ -122,12 +140,27 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): Returns: Workout: Updated Workout instance """ + # Add date and planned check + # Check if date is valid + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + + if validated_data["planned"]: + if timeNowAdjusted >= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be a future date"]}) + else: + if timeNowAdjusted <= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be an old date"]}) + exercise_instances_data = validated_data.pop("exercise_instances") exercise_instances = instance.exercise_instances instance.name = validated_data.get("name", instance.name) instance.notes = validated_data.get("notes", instance.notes) - instance.visibility = validated_data.get("visibility", instance.visibility) + instance.visibility = validated_data.get( + "visibility", instance.visibility) instance.date = validated_data.get("date", instance.date) instance.save() diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf40454376b23d233f9fe2cecaf9da43fddb8..2026d46fb1f6a9638a4ed23d997ef10cfb26a7d5 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -31,8 +31,11 @@ from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.response import Response import json from collections import namedtuple -import base64, pickle +import base64 +import pickle from django.core.signing import Signer +from datetime import datetime +import pytz @api_view(["GET"]) @@ -141,6 +144,16 @@ class WorkoutList( Q(visibility="PU") | (Q(visibility="CO") & Q(owner__coach=self.request.user)) ).distinct() + # Check if the planned workout has happened + if len(qs) > 0: + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + for i in range(0, len(qs)): + if qs[i].planned: + if timeNowAdjusted > qs[i].date: + # Update: set planned to false + qs[i].planned = False + qs[i].save() return qs @@ -155,7 +168,6 @@ class WorkoutDetail( HTTP methods: GET, PUT, DELETE """ - queryset = Workout.objects.all() serializer_class = WorkoutSerializer permission_classes = [ diff --git a/frontend/www/plannedWorkout.html b/frontend/www/plannedWorkout.html new file mode 100644 index 0000000000000000000000000000000000000000..f66b88c6b0e77fb44ff64a7465336dc7042fafa6 --- /dev/null +++ b/frontend/www/plannedWorkout.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Workout</title> + + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> + + <script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script> + <link rel="stylesheet" href="styles/style.css"> + <script src="scripts/navbar.js" type="text/javascript" defer></script> +</head> +<body> + <navbar-el></navbar-el> + + <div class="container"> + <div class="row"> + <div class="col-lg"> + <h3 class="mt-3">View/Edit Planned Workout</h3> + </div> + </div> + <div class="row"> + <div class="col-lg"> + <p class="mt-4">A planned workout is a future workout that will be autologged</h3> + </div> + </div> + <form class="row g-3 mb-4" id="form-workout"> + <div class="col-lg-6 "> + <label for="inputName" class="form-label">Name</label> + <input type="text" class="form-control" id="inputName" name="name" readonly> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputDateTime" class="form-label">Date/Time</label> + <input type="datetime-local" class="form-control" id="inputDateTime" name="date" readonly> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputOwner" class="form-label">Owner</label> + <input type="text" class="form-control" id="inputOwner" name="owner_username" readonly> + </div> + <div class="col-lg-6"> + <label for="inputVisibility" class="form-label">Visibility</label> + <select id="inputVisibility" class="form-select" name="visibility" disabled> + <option value="PU">Public</option> + <option value="CO">Coach</option> + <option value="PR">Private</option> + </select> + </div> + <div class="col-lg-6"> + <label for="inputNotes" class="form-label">Notes</label> + <textarea class="form-control" id="inputNotes" name="notes" readonly></textarea> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <div class="input-group"> + <input type="file" class="form-control" id="customFile" name="files" multiple disabled> + </div> + <div id="uploaded-files" class="ms-1 mt-2"> + </div> + </div> + <div class="col-lg-6"> + </div> + <div class="col-lg-6"> + <input type="button" class="btn btn-primary hide" id="btn-ok-workout" value=" OK "> + <input type="button" class="btn btn-primary hide" id="btn-edit-workout" value=" Edit "> + <input type="button" class="btn btn-secondary hide" id="btn-cancel-workout" value="Cancel"> + <input type="button" class="btn btn-danger float-end hide" id="btn-delete-workout" value="Delete"> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-12"> + <h3 class="mt-3">Exercises</h3> + </div> + <div id="div-exercises" class="col-lg-12"> + </div> + <div class="col-lg-6"> + <input type="button" class="btn btn-primary hide" id="btn-add-exercise" value="Add exercise"> + <input type="button" class="btn btn-danger hide" id="btn-remove-exercise" value="Remove exercise"> + </div> + <div class="col-lg-6"></div> + + </form> + <div class="row bootstrap snippets bootdeys" id="div-comment-row"> + <div class="col-md-8 col-sm-12"> + <div class="comment-wrapper"> + <div class="card"> + <div class="card-header bg-primary text-light"> + Comment panel + </div> + <div class="card-body"> + <textarea class="form-control" id="comment-area" placeholder="write a comment..." rows="3"></textarea> + <br> + <button type="button" id="post-comment" class="btn btn-info pull-right">Post</button> + <div class="clearfix"></div> + <hr> + <ul id="comment-list" class="list-unstyled"> + </ul> + </div> + </div> + </div> + + </div> + </div> + </div> + + <template id="template-exercise"> + <div class="row div-exercise-container g-3 mb-3"> + <div class="col-lg-6"><h5>Exercise</h5></div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label class="form-label exercise-type">Type</label> + <select class="form-select" name="type"> + </select> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-3"> + <label class="form-label exercise-sets">Sets</label> + <input type="number" class="form-control" name="sets"> + </div> + <div class="col-lg-3"> + <label class="form-label exercise-number">Number</label> + <input type="number" class="form-control" name="number"> + </div> + <div class="col-lg-6"></div> + </div> + </template> + + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/plannedWorkout.js"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script> + </body> +</html> \ No newline at end of file diff --git a/frontend/www/scripts/plannedWorkout.js b/frontend/www/scripts/plannedWorkout.js new file mode 100644 index 0000000000000000000000000000000000000000..da55ea239787a25d9591b7f1e7e3465238d560f3 --- /dev/null +++ b/frontend/www/scripts/plannedWorkout.js @@ -0,0 +1,426 @@ +let cancelWorkoutButton; +let okWorkoutButton; +let deleteWorkoutButton; +let editWorkoutButton; +let postCommentButton; + +async function retrieveWorkout(id) { + let workoutData = null; + let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve workout data!", data); + document.body.prepend(alert); + } else { + workoutData = await response.json(); + let form = document.querySelector("#form-workout"); + let formData = new FormData(form); + + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = workoutData[key]; + if (key == "date") { + // Creating a valid datetime-local string with the correct local time + let date = new Date(newVal); + date = new Date( + date.getTime() - date.getTimezoneOffset() * 60 * 1000 + ).toISOString(); // get ISO format for local time + newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) + } + if (key != "files") { + input.value = newVal; + } + } + + let input = form.querySelector("select:disabled"); + input.value = workoutData["visibility"]; + // files + let filesDiv = document.querySelector("#uploaded-files"); + for (let file of workoutData.files) { + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + filesDiv.appendChild(a); + } + + // create exercises + + // fetch exercise types + let exerciseTypeResponse = await sendRequest( + "GET", + `${HOST}/api/exercises/` + ); + let exerciseTypes = await exerciseTypeResponse.json(); + + //TODO: This should be in its own method. + for (let i = 0; i < workoutData.exercise_instances.length; i++) { + let templateExercise = document.querySelector("#template-exercise"); + let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode( + true + ); + + let exerciseTypeLabel = divExerciseContainer.querySelector( + ".exercise-type" + ); + exerciseTypeLabel.for = `inputExerciseType${i}`; + + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + exerciseTypeSelect.id = `inputExerciseType${i}`; + exerciseTypeSelect.disabled = true; + + let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); + let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; + let currentExerciseType = ""; + + for (let j = 0; j < exerciseTypes.count; j++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[j].id; + if (currentExerciseTypeId == exerciseTypes.results[j].id) { + currentExerciseType = exerciseTypes.results[j]; + } + option.innerText = exerciseTypes.results[j].name; + exerciseTypeSelect.append(option); + } + + exerciseTypeSelect.value = currentExerciseType.id; + + let exerciseSetLabel = divExerciseContainer.querySelector( + ".exercise-sets" + ); + exerciseSetLabel.for = `inputSets${i}`; + + let exerciseSetInput = divExerciseContainer.querySelector( + "input[name='sets']" + ); + exerciseSetInput.id = `inputSets${i}`; + exerciseSetInput.value = workoutData.exercise_instances[i].sets; + exerciseSetInput.readOnly = true; + + let exerciseNumberLabel = divExerciseContainer.querySelector( + ".exercise-number" + ); + (exerciseNumberLabel.for = "for"), `inputNumber${i}`; + exerciseNumberLabel.innerText = currentExerciseType.unit; + + let exerciseNumberInput = divExerciseContainer.querySelector( + "input[name='number']" + ); + exerciseNumberInput.id = `inputNumber${i}`; + exerciseNumberInput.value = workoutData.exercise_instances[i].number; + exerciseNumberInput.readOnly = true; + + let exercisesDiv = document.querySelector("#div-exercises"); + exercisesDiv.appendChild(divExerciseContainer); + } + } + return workoutData; +} + +function handleCancelDuringWorkoutEdit() { + location.reload(); +} + +function handleEditWorkoutButtonClick() { + let addExerciseButton = document.querySelector("#btn-add-exercise"); + let removeExerciseButton = document.querySelector("#btn-remove-exercise"); + + setReadOnly(false, "#form-workout"); + document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly + + editWorkoutButton.className += " hide"; + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + deleteWorkoutButton.className = deleteWorkoutButton.className.replace( + " hide", + "" + ); + addExerciseButton.className = addExerciseButton.className.replace( + " hide", + "" + ); + removeExerciseButton.className = removeExerciseButton.className.replace( + " hide", + "" + ); + + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); +} + +async function deleteWorkout(id) { + let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete workout ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("workouts.html"); + } +} + +async function updateWorkout(id) { + let submitForm = generateWorkoutForm(); + + let response = await sendRequest( + "PUT", + `${HOST}/api/workouts/${id}/`, + submitForm, + "" + ); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not update workout!", data); + document.body.prepend(alert); + } else { + location.reload(); + } +} + +function generateWorkoutForm() { + // TODO: Add check for future date + var today = new Date().toISOString(); + + document.querySelector("#inputDateTime").min = today; + + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get("name")); + let date = new Date(formData.get("date")).toISOString(); + submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("visibility", formData.get("visibility")); + submitForm.append("planned", true); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i], + }); + } + + submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("files", file); + } + return submitForm; +} + +async function createWorkout() { + let submitForm = generateWorkoutForm(); + + let response = await sendRequest( + "POST", + `${HOST}/api/workouts/`, + submitForm, + "" + ); + + if (response.ok) { + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } +} + +function handleCancelDuringWorkoutCreate() { + window.location.replace("workouts.html"); +} + +async function createBlankExercise() { + let form = document.querySelector("#form-workout"); + + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); + + let exerciseTemplate = document.querySelector("#template-exercise"); + let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode( + true + ); + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + + for (let i = 0; i < exerciseTypes.count; i++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[i].id; + option.innerText = exerciseTypes.results[i].name; + exerciseTypeSelect.append(option); + } + + let currentExerciseType = exerciseTypes.results[0]; + exerciseTypeSelect.value = currentExerciseType.name; + + let divExercises = document.querySelector("#div-exercises"); + divExercises.appendChild(divExerciseContainer); +} + +function removeExercise(event) { + let divExerciseContainers = document.querySelectorAll( + ".div-exercise-container" + ); + if (divExerciseContainers && divExerciseContainers.length > 0) { + divExerciseContainers[divExerciseContainers.length - 1].remove(); + } +} + +function addComment(author, text, date, append) { + /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ + let commentList = document.querySelector("#comment-list"); + let listElement = document.createElement("li"); + listElement.className = "media"; + let commentBody = document.createElement("div"); + commentBody.className = "media-body"; + let dateSpan = document.createElement("span"); + dateSpan.className = "text-muted pull-right me-1"; + let smallText = document.createElement("small"); + smallText.className = "text-muted"; + + if (date != "Now") { + let localDate = new Date(date); + smallText.innerText = localDate.toLocaleString(); + } else { + smallText.innerText = date; + } + + dateSpan.appendChild(smallText); + commentBody.appendChild(dateSpan); + + let strong = document.createElement("strong"); + strong.className = "text-success"; + strong.innerText = author; + commentBody.appendChild(strong); + let p = document.createElement("p"); + p.innerHTML = text; + + commentBody.appendChild(strong); + commentBody.appendChild(p); + listElement.appendChild(commentBody); + + if (append) { + commentList.append(listElement); + } else { + commentList.prepend(listElement); + } +} + +async function createComment(workoutid) { + let commentArea = document.querySelector("#comment-area"); + let content = commentArea.value; + let body = { + workout: `${HOST}/api/workouts/${workoutid}/`, + content: content, + }; + + let response = await sendRequest("POST", `${HOST}/api/comments/`, body); + if (response.ok) { + addComment(sessionStorage.getItem("username"), content, "Now", false); + } else { + let data = await response.json(); + let alert = createAlert("Failed to create comment!", data); + document.body.prepend(alert); + } +} + +async function retrieveComments(workoutid) { + let response = await sendRequest("GET", `${HOST}/api/comments/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve comments!", data); + document.body.prepend(alert); + } else { + let data = await response.json(); + let comments = data.results; + for (let comment of comments) { + let splitArray = comment.workout.split("/"); + if (splitArray[splitArray.length - 2] == workoutid) { + addComment(comment.owner, comment.content, comment.timestamp, true); + } + } + } +} + +window.addEventListener("DOMContentLoaded", async () => { + cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); + okWorkoutButton = document.querySelector("#btn-ok-workout"); + deleteWorkoutButton = document.querySelector("#btn-delete-workout"); + editWorkoutButton = document.querySelector("#btn-edit-workout"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddExercise = document.querySelector("#btn-add-exercise"); + let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); + + buttonAddExercise.addEventListener("click", createBlankExercise); + buttonRemoveExercise.addEventListener("click", removeExercise); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + if (urlParams.has("id")) { + const id = urlParams.get("id"); + let workoutData = await retrieveWorkout(id); + await retrieveComments(id); + + if (workoutData["owner"] == currentUser.url) { + editWorkoutButton.classList.remove("hide"); + editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + deleteWorkoutButton.addEventListener( + "click", + (async (id) => await deleteWorkout(id)).bind(undefined, id) + ); + okWorkoutButton.addEventListener( + "click", + (async (id) => await updateWorkout(id)).bind(undefined, id) + ); + postCommentButton.addEventListener( + "click", + (async (id) => await createComment(id)).bind(undefined, id) + ); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); + } + } else { + await createBlankExercise(); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + setReadOnly(false, "#form-workout"); + ownerInput.readOnly = !ownerInput.readOnly; + + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + buttonAddExercise.className = buttonAddExercise.className.replace( + " hide", + "" + ); + buttonRemoveExercise.className = buttonRemoveExercise.className.replace( + " hide", + "" + ); + + okWorkoutButton.addEventListener( + "click", + async () => await createWorkout() + ); + cancelWorkoutButton.addEventListener( + "click", + handleCancelDuringWorkoutCreate + ); + divCommentRow.className += " hide"; + } +}); diff --git a/frontend/www/scripts/workout.js b/frontend/www/scripts/workout.js index 9cb67115417e25a1117118edab54121d86f7aed0..8123d50937a9ec5a92d90bdec8426681a0409502 100644 --- a/frontend/www/scripts/workout.js +++ b/frontend/www/scripts/workout.js @@ -4,440 +4,519 @@ let deleteWorkoutButton; let editWorkoutButton; let exportWorkoutButton; let postCommentButton; +let planned = false; async function retrieveWorkout(id) { - let workoutData = null; - let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not retrieve workout data!", data); - document.body.prepend(alert); - } else { - workoutData = await response.json(); - let form = document.querySelector("#form-workout"); - let formData = new FormData(form); - - for (let key of formData.keys()) { - let selector = `input[name="${key}"], textarea[name="${key}"]`; - let input = form.querySelector(selector); - let newVal = workoutData[key]; - if (key == "date") { - // Creating a valid datetime-local string with the correct local time - let date = new Date(newVal); - date = new Date(date.getTime() - (date.getTimezoneOffset() * 60 * 1000)).toISOString(); // get ISO format for local time - newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) - } - if (key != "files") { - input.value = newVal; - } - } - - let input = form.querySelector("select:disabled"); - input.value = workoutData["visibility"]; - // files - let filesDiv = document.querySelector("#uploaded-files"); - for (let file of workoutData.files) { - let a = document.createElement("a"); - a.href = file.file; - let pathArray = file.file.split("/"); - a.text = pathArray[pathArray.length - 1]; - a.className = "me-2"; - filesDiv.appendChild(a); - } - - // create exercises - - // fetch exercise types - let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); - let exerciseTypes = await exerciseTypeResponse.json(); - - //TODO: This should be in its own method. - for (let i = 0; i < workoutData.exercise_instances.length; i++) { - let templateExercise = document.querySelector("#template-exercise"); - let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode(true); - - let exerciseTypeLabel = divExerciseContainer.querySelector('.exercise-type'); - exerciseTypeLabel.for = `inputExerciseType${i}`; - - let exerciseTypeSelect = divExerciseContainer.querySelector("select"); - exerciseTypeSelect.id = `inputExerciseType${i}`; - exerciseTypeSelect.disabled = true; + let workoutData = null; + let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve workout data!", data); + document.body.prepend(alert); + } else { + workoutData = await response.json(); + let form = document.querySelector("#form-workout"); + let formData = new FormData(form); + planned = workoutData.planned; + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = workoutData[key]; + if (key == "date") { + // Creating a valid datetime-local string with the correct local time + let date = new Date(newVal); + date = new Date( + date.getTime() - date.getTimezoneOffset() * 60 * 1000 + ).toISOString(); // get ISO format for local time + newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) + } + if (key != "files") { + input.value = newVal; + } + } - let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); - let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; - let currentExerciseType = ""; + let input = form.querySelector("select:disabled"); + input.value = workoutData["visibility"]; + // files + let filesDiv = document.querySelector("#uploaded-files"); + for (let file of workoutData.files) { + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + filesDiv.appendChild(a); + } - for (let j = 0; j < exerciseTypes.count; j++) { - let option = document.createElement("option"); - option.value = exerciseTypes.results[j].id; - if (currentExerciseTypeId == exerciseTypes.results[j].id) { - currentExerciseType = exerciseTypes.results[j]; - } - option.innerText = exerciseTypes.results[j].name; - exerciseTypeSelect.append(option); - } + // create exercises - exerciseTypeSelect.value = currentExerciseType.id; + // fetch exercise types + let exerciseTypeResponse = await sendRequest( + "GET", + `${HOST}/api/exercises/` + ); + let exerciseTypes = await exerciseTypeResponse.json(); - let exerciseSetLabel = divExerciseContainer.querySelector('.exercise-sets'); - exerciseSetLabel.for = `inputSets${i}`; + //TODO: This should be in its own method. + for (let i = 0; i < workoutData.exercise_instances.length; i++) { + let templateExercise = document.querySelector("#template-exercise"); + let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode( + true + ); - let exerciseSetInput = divExerciseContainer.querySelector("input[name='sets']"); - exerciseSetInput.id = `inputSets${i}`; - exerciseSetInput.value = workoutData.exercise_instances[i].sets; - exerciseSetInput.readOnly = true; + let exerciseTypeLabel = divExerciseContainer.querySelector( + ".exercise-type" + ); + exerciseTypeLabel.for = `inputExerciseType${i}`; - let exerciseNumberLabel = divExerciseContainer.querySelector('.exercise-number'); - exerciseNumberLabel.for = "for", `inputNumber${i}`; - exerciseNumberLabel.innerText = currentExerciseType.unit; + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + exerciseTypeSelect.id = `inputExerciseType${i}`; + exerciseTypeSelect.disabled = true; - let exerciseNumberInput = divExerciseContainer.querySelector("input[name='number']"); - exerciseNumberInput.id = `inputNumber${i}`; - exerciseNumberInput.value = workoutData.exercise_instances[i].number; - exerciseNumberInput.readOnly = true; + let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); + let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; + let currentExerciseType = ""; - let exercisesDiv = document.querySelector("#div-exercises"); - exercisesDiv.appendChild(divExerciseContainer); + for (let j = 0; j < exerciseTypes.count; j++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[j].id; + if (currentExerciseTypeId == exerciseTypes.results[j].id) { + currentExerciseType = exerciseTypes.results[j]; } + option.innerText = exerciseTypes.results[j].name; + exerciseTypeSelect.append(option); + } + + exerciseTypeSelect.value = currentExerciseType.id; + + let exerciseSetLabel = divExerciseContainer.querySelector( + ".exercise-sets" + ); + exerciseSetLabel.for = `inputSets${i}`; + + let exerciseSetInput = divExerciseContainer.querySelector( + "input[name='sets']" + ); + exerciseSetInput.id = `inputSets${i}`; + exerciseSetInput.value = workoutData.exercise_instances[i].sets; + exerciseSetInput.readOnly = true; + + let exerciseNumberLabel = divExerciseContainer.querySelector( + ".exercise-number" + ); + (exerciseNumberLabel.for = "for"), `inputNumber${i}`; + exerciseNumberLabel.innerText = currentExerciseType.unit; + + let exerciseNumberInput = divExerciseContainer.querySelector( + "input[name='number']" + ); + exerciseNumberInput.id = `inputNumber${i}`; + exerciseNumberInput.value = workoutData.exercise_instances[i].number; + exerciseNumberInput.readOnly = true; + + let exercisesDiv = document.querySelector("#div-exercises"); + exercisesDiv.appendChild(divExerciseContainer); } - return workoutData; + } + return workoutData; } function handleCancelDuringWorkoutEdit() { - location.reload(); + location.reload(); } function handleEditWorkoutButtonClick() { - let addExerciseButton = document.querySelector("#btn-add-exercise"); - let removeExerciseButton = document.querySelector("#btn-remove-exercise"); - - setReadOnly(false, "#form-workout"); - document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly - - editWorkoutButton.className += " hide"; - exportWorkoutButton.className += " hide"; - okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); - cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); - deleteWorkoutButton.className = deleteWorkoutButton.className.replace(" hide", ""); - addExerciseButton.className = addExerciseButton.className.replace(" hide", ""); - removeExerciseButton.className = removeExerciseButton.className.replace(" hide", ""); - - cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); - + let addExerciseButton = document.querySelector("#btn-add-exercise"); + let removeExerciseButton = document.querySelector("#btn-remove-exercise"); + + setReadOnly(false, "#form-workout"); + document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly + + editWorkoutButton.className += " hide"; + exportWorkoutButton.className += " hide"; + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + deleteWorkoutButton.className = deleteWorkoutButton.className.replace( + " hide", + "" + ); + addExerciseButton.className = addExerciseButton.className.replace( + " hide", + "" + ); + removeExerciseButton.className = removeExerciseButton.className.replace( + " hide", + "" + ); + + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function handleExportToCalendarClick(workoutData) { - - const headers = { - subject: "Subject", - startDate: "Start date", - startTime: "Start time", - description: "Description" - } - - const dataFormatted = [] - - const startTime = new Date(workoutData.date).toLocaleTimeString("en-us") - const startDate = new Date(workoutData.date).toLocaleString('en-us', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }).replace(/(\d+)\/(\d+)\/(\d+)/, '$1/$2/$3') - - - dataFormatted.push({ - subject: workoutData.name, - startDate: startDate, - startTime: startTime, - description: workoutData.notes + const headers = { + subject: "Subject", + startDate: "Start date", + startTime: "Start time", + description: "Description", + }; + + const dataFormatted = []; + + const startTime = new Date(workoutData.date).toLocaleTimeString("en-us"); + const startDate = new Date(workoutData.date) + .toLocaleString("en-us", { + year: "numeric", + month: "2-digit", + day: "2-digit", }) + .replace(/(\d+)\/(\d+)\/(\d+)/, "$1/$2/$3"); + dataFormatted.push({ + subject: workoutData.name, + startDate: startDate, + startTime: startTime, + description: workoutData.notes, + }); - console.log(dataFormatted) + console.log(dataFormatted); - exportCSVFile(headers, dataFormatted, "event") + exportCSVFile(headers, dataFormatted, "event"); } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function convertToCSV(objArray) { - var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray; - var str = ''; - - for (var i = 0; i < array.length; i++) { - var line = ''; - for (var index in array[i]) { - if (line != '') line += ',' + var array = typeof objArray != "object" ? JSON.parse(objArray) : objArray; + var str = ""; - line += array[i][index]; - } + for (var i = 0; i < array.length; i++) { + var line = ""; + for (var index in array[i]) { + if (line != "") line += ","; - str += line + '\r\n'; + line += array[i][index]; } - return str; + str += line + "\r\n"; + } + + return str; } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function exportCSVFile(headers, items, fileTitle) { - - console.log(items, headers) - if (headers) { - items.unshift(headers); - } - - // Convert Object to JSON - var jsonObject = JSON.stringify(items); - - var csv = this.convertToCSV(jsonObject); - - var exportedFilenmae = fileTitle + '.csv' || 'export.csv'; - - var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'}); - if (navigator.msSaveBlob) { // IE 10+ - navigator.msSaveBlob(blob, exportedFilenmae); - } else { - var link = document.createElement("a"); - if (link.download !== undefined) { // feature detection - // Browsers that support HTML5 download attribute - var url = URL.createObjectURL(blob); - link.setAttribute("href", url); - link.setAttribute("download", exportedFilenmae); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } + console.log(items, headers); + if (headers) { + items.unshift(headers); + } + + // Convert Object to JSON + var jsonObject = JSON.stringify(items); + + var csv = this.convertToCSV(jsonObject); + + var exportedFilenmae = fileTitle + ".csv" || "export.csv"; + + var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + if (navigator.msSaveBlob) { + // IE 10+ + navigator.msSaveBlob(blob, exportedFilenmae); + } else { + var link = document.createElement("a"); + if (link.download !== undefined) { + // feature detection + // Browsers that support HTML5 download attribute + var url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", exportedFilenmae); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); } + } } async function deleteWorkout(id) { - let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert(`Could not delete workout ${id}!`, data); - document.body.prepend(alert); - } else { - window.location.replace("workouts.html"); - } + let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete workout ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("workouts.html"); + } } async function updateWorkout(id) { - let submitForm = generateWorkoutForm(); - - let response = await sendRequest("PUT", `${HOST}/api/workouts/${id}/`, submitForm, ""); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not update workout!", data); - document.body.prepend(alert); - } else { - location.reload(); - } + let submitForm = generateWorkoutForm(); + let response = await sendRequest( + "PUT", + `${HOST}/api/workouts/${id}/`, + submitForm, + "" + ); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not update workout!", data); + document.body.prepend(alert); + } else { + location.reload(); + } } function generateWorkoutForm() { - let form = document.querySelector("#form-workout"); - - let formData = new FormData(form); - let submitForm = new FormData(); - - submitForm.append("name", formData.get('name')); - let date = new Date(formData.get('date')).toISOString(); - submitForm.append("date", date); - submitForm.append("notes", formData.get("notes")); - submitForm.append("visibility", formData.get("visibility")); - - // adding exercise instances - let exerciseInstances = []; - let exerciseInstancesTypes = formData.getAll("type"); - let exerciseInstancesSets = formData.getAll("sets"); - let exerciseInstancesNumbers = formData.getAll("number"); - for (let i = 0; i < exerciseInstancesTypes.length; i++) { - exerciseInstances.push({ - exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, - number: exerciseInstancesNumbers[i], - sets: exerciseInstancesSets[i] - }); - } - - submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); - // adding files - for (let file of formData.getAll("files")) { - submitForm.append("files", file); - } - return submitForm; + var today = new Date().toISOString(); + + document.querySelector("#inputDateTime").min = today; + + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get("name")); + let date = new Date(formData.get("date")).toISOString(); + submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("visibility", formData.get("visibility")); + + submitForm.append("planned", planned); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i], + }); + } + + submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("files", file); + } + return submitForm; } async function createWorkout() { - let submitForm = generateWorkoutForm(); + let submitForm = generateWorkoutForm(); - let response = await sendRequest("POST", `${HOST}/api/workouts/`, submitForm, ""); + let response = await sendRequest( + "POST", + `${HOST}/api/workouts/`, + submitForm, + "" + ); - if (response.ok) { - window.location.replace("workouts.html"); - } else { - let data = await response.json(); - let alert = createAlert("Could not create new workout!", data); - document.body.prepend(alert); - } + if (response.ok) { + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } } function handleCancelDuringWorkoutCreate() { - window.location.replace("workouts.html"); + window.location.replace("workouts.html"); } async function createBlankExercise() { - let form = document.querySelector("#form-workout"); + let form = document.querySelector("#form-workout"); - let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); - let exerciseTypes = await exerciseTypeResponse.json(); + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); - let exerciseTemplate = document.querySelector("#template-exercise"); - let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode(true); - let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + let exerciseTemplate = document.querySelector("#template-exercise"); + let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode( + true + ); + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); - for (let i = 0; i < exerciseTypes.count; i++) { - let option = document.createElement("option"); - option.value = exerciseTypes.results[i].id; - option.innerText = exerciseTypes.results[i].name; - exerciseTypeSelect.append(option); - } + for (let i = 0; i < exerciseTypes.count; i++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[i].id; + option.innerText = exerciseTypes.results[i].name; + exerciseTypeSelect.append(option); + } - let currentExerciseType = exerciseTypes.results[0]; - exerciseTypeSelect.value = currentExerciseType.name; + let currentExerciseType = exerciseTypes.results[0]; + exerciseTypeSelect.value = currentExerciseType.name; - let divExercises = document.querySelector("#div-exercises"); - divExercises.appendChild(divExerciseContainer); + let divExercises = document.querySelector("#div-exercises"); + divExercises.appendChild(divExerciseContainer); } function removeExercise(event) { - let divExerciseContainers = document.querySelectorAll(".div-exercise-container"); - if (divExerciseContainers && divExerciseContainers.length > 0) { - divExerciseContainers[divExerciseContainers.length - 1].remove(); - } + let divExerciseContainers = document.querySelectorAll( + ".div-exercise-container" + ); + if (divExerciseContainers && divExerciseContainers.length > 0) { + divExerciseContainers[divExerciseContainers.length - 1].remove(); + } } function addComment(author, text, date, append) { - /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ - let commentList = document.querySelector("#comment-list"); - let listElement = document.createElement("li"); - listElement.className = "media"; - let commentBody = document.createElement("div"); - commentBody.className = "media-body"; - let dateSpan = document.createElement("span"); - dateSpan.className = "text-muted pull-right me-1"; - let smallText = document.createElement("small"); - smallText.className = "text-muted"; - - if (date != "Now") { - let localDate = new Date(date); - smallText.innerText = localDate.toLocaleString(); - } else { - smallText.innerText = date; - } - - dateSpan.appendChild(smallText); - commentBody.appendChild(dateSpan); - - let strong = document.createElement("strong"); - strong.className = "text-success"; - strong.innerText = author; - commentBody.appendChild(strong); - let p = document.createElement("p"); - p.innerHTML = text; - - commentBody.appendChild(strong); - commentBody.appendChild(p); - listElement.appendChild(commentBody); - - if (append) { - commentList.append(listElement); - } else { - commentList.prepend(listElement); - } - + /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ + let commentList = document.querySelector("#comment-list"); + let listElement = document.createElement("li"); + listElement.className = "media"; + let commentBody = document.createElement("div"); + commentBody.className = "media-body"; + let dateSpan = document.createElement("span"); + dateSpan.className = "text-muted pull-right me-1"; + let smallText = document.createElement("small"); + smallText.className = "text-muted"; + + if (date != "Now") { + let localDate = new Date(date); + smallText.innerText = localDate.toLocaleString(); + } else { + smallText.innerText = date; + } + + dateSpan.appendChild(smallText); + commentBody.appendChild(dateSpan); + + let strong = document.createElement("strong"); + strong.className = "text-success"; + strong.innerText = author; + commentBody.appendChild(strong); + let p = document.createElement("p"); + p.innerHTML = text; + + commentBody.appendChild(strong); + commentBody.appendChild(p); + listElement.appendChild(commentBody); + + if (append) { + commentList.append(listElement); + } else { + commentList.prepend(listElement); + } } async function createComment(workoutid) { - let commentArea = document.querySelector("#comment-area"); - let content = commentArea.value; - let body = {workout: `${HOST}/api/workouts/${workoutid}/`, content: content}; - - let response = await sendRequest("POST", `${HOST}/api/comments/`, body); - if (response.ok) { - addComment(sessionStorage.getItem("username"), content, "Now", false); - } else { - let data = await response.json(); - let alert = createAlert("Failed to create comment!", data); - document.body.prepend(alert); - } + let commentArea = document.querySelector("#comment-area"); + let content = commentArea.value; + let body = { + workout: `${HOST}/api/workouts/${workoutid}/`, + content: content, + }; + + let response = await sendRequest("POST", `${HOST}/api/comments/`, body); + if (response.ok) { + addComment(sessionStorage.getItem("username"), content, "Now", false); + } else { + let data = await response.json(); + let alert = createAlert("Failed to create comment!", data); + document.body.prepend(alert); + } } async function retrieveComments(workoutid) { - let response = await sendRequest("GET", `${HOST}/api/comments/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not retrieve comments!", data); - document.body.prepend(alert); - } else { - let data = await response.json(); - let comments = data.results; - for (let comment of comments) { - let splitArray = comment.workout.split("/"); - if (splitArray[splitArray.length - 2] == workoutid) { - addComment(comment.owner, comment.content, comment.timestamp, true); - } - } + let response = await sendRequest("GET", `${HOST}/api/comments/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve comments!", data); + document.body.prepend(alert); + } else { + let data = await response.json(); + let comments = data.results; + for (let comment of comments) { + let splitArray = comment.workout.split("/"); + if (splitArray[splitArray.length - 2] == workoutid) { + addComment(comment.owner, comment.content, comment.timestamp, true); + } } + } } window.addEventListener("DOMContentLoaded", async () => { - cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); - okWorkoutButton = document.querySelector("#btn-ok-workout"); - deleteWorkoutButton = document.querySelector("#btn-delete-workout"); - editWorkoutButton = document.querySelector("#btn-edit-workout"); - exportWorkoutButton = document.querySelector("#btn-export-workout"); - let postCommentButton = document.querySelector("#post-comment"); - let divCommentRow = document.querySelector("#div-comment-row"); - let buttonAddExercise = document.querySelector("#btn-add-exercise"); - let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); - - buttonAddExercise.addEventListener("click", createBlankExercise); - buttonRemoveExercise.addEventListener("click", removeExercise); - - const urlParams = new URLSearchParams(window.location.search); - let currentUser = await getCurrentUser(); - - if (urlParams.has('id')) { - const id = urlParams.get('id'); - let workoutData = await retrieveWorkout(id); - await retrieveComments(id); - - if (workoutData["owner"] == currentUser.url) { - editWorkoutButton.classList.remove("hide"); - exportWorkoutButton.classList.remove("hide"); - editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); - exportWorkoutButton.addEventListener("click", ((workoutData) => handleExportToCalendarClick(workoutData)).bind(undefined, workoutData)); - deleteWorkoutButton.addEventListener("click", (async (id) => await deleteWorkout(id)).bind(undefined, id)); - okWorkoutButton.addEventListener("click", (async (id) => await updateWorkout(id)).bind(undefined, id)); - postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); - divCommentRow.className = divCommentRow.className.replace(" hide", ""); - } - } else { - await createBlankExercise(); - let ownerInput = document.querySelector("#inputOwner"); - ownerInput.value = currentUser.username; - setReadOnly(false, "#form-workout"); - ownerInput.readOnly = !ownerInput.readOnly; - - okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); - cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); - buttonAddExercise.className = buttonAddExercise.className.replace(" hide", ""); - buttonRemoveExercise.className = buttonRemoveExercise.className.replace(" hide", ""); - - okWorkoutButton.addEventListener("click", async () => await createWorkout()); - cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutCreate); - divCommentRow.className += " hide"; + cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); + okWorkoutButton = document.querySelector("#btn-ok-workout"); + deleteWorkoutButton = document.querySelector("#btn-delete-workout"); + editWorkoutButton = document.querySelector("#btn-edit-workout"); + exportWorkoutButton = document.querySelector("#btn-export-workout"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddExercise = document.querySelector("#btn-add-exercise"); + let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); + + buttonAddExercise.addEventListener("click", createBlankExercise); + buttonRemoveExercise.addEventListener("click", removeExercise); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + if (urlParams.has("id")) { + const id = urlParams.get("id"); + let workoutData = await retrieveWorkout(id); + await retrieveComments(id); + + if (workoutData["owner"] == currentUser.url) { + editWorkoutButton.classList.remove("hide"); + exportWorkoutButton.classList.remove("hide"); + editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + exportWorkoutButton.addEventListener( + "click", + ((workoutData) => handleExportToCalendarClick(workoutData)).bind( + undefined, + workoutData + ) + ); + deleteWorkoutButton.addEventListener( + "click", + (async (id) => await deleteWorkout(id)).bind(undefined, id) + ); + okWorkoutButton.addEventListener( + "click", + (async (id) => await updateWorkout(id)).bind(undefined, id) + ); + postCommentButton.addEventListener( + "click", + (async (id) => await createComment(id)).bind(undefined, id) + ); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); } + } else { + await createBlankExercise(); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + setReadOnly(false, "#form-workout"); + ownerInput.readOnly = !ownerInput.readOnly; -}); \ No newline at end of file + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + buttonAddExercise.className = buttonAddExercise.className.replace( + " hide", + "" + ); + buttonRemoveExercise.className = buttonRemoveExercise.className.replace( + " hide", + "" + ); + + okWorkoutButton.addEventListener( + "click", + async () => await createWorkout() + ); + cancelWorkoutButton.addEventListener( + "click", + handleCancelDuringWorkoutCreate + ); + divCommentRow.className += " hide"; + } +}); diff --git a/frontend/www/scripts/workouts.js b/frontend/www/scripts/workouts.js index 772be1ea070499ad9574d787d990c1ca17097bdf..d18ba4e2608917456bc088d8e46c578ab2fd792f 100644 --- a/frontend/www/scripts/workouts.js +++ b/frontend/www/scripts/workouts.js @@ -1,106 +1,146 @@ async function fetchWorkouts(ordering) { - let response = await sendRequest("GET", `${HOST}/api/workouts/?ordering=${ordering}`); + let response = await sendRequest( + "GET", + `${HOST}/api/workouts/?ordering=${ordering}` + ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } else { - let data = await response.json(); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + let data = await response.json(); - let workouts = data.results; - let container = document.getElementById('div-content'); - workouts.forEach(workout => { - let templateWorkout = document.querySelector("#template-workout"); - let cloneWorkout = templateWorkout.content.cloneNode(true); + let workouts = data.results; + let container = document.getElementById("div-content"); + workouts.forEach((workout) => { + let templateWorkout = document.querySelector("#template-workout"); + let cloneWorkout = templateWorkout.content.cloneNode(true); - let aWorkout = cloneWorkout.querySelector("a"); - aWorkout.href = `workout.html?id=${workout.id}`; + let aWorkout = cloneWorkout.querySelector("a"); + aWorkout.href = `workout.html?id=${workout.id}`; - let h5 = aWorkout.querySelector("h5"); - h5.textContent = workout.name; + let h5 = aWorkout.querySelector("h5"); + h5.textContent = workout.name; - let localDate = new Date(workout.date); + let localDate = new Date(workout.date); - let table = aWorkout.querySelector("table"); - let rows = table.querySelectorAll("tr"); - rows[0].querySelectorAll("td")[1].textContent = localDate.toLocaleDateString(); // Date - rows[1].querySelectorAll("td")[1].textContent = localDate.toLocaleTimeString(); // Time - rows[2].querySelectorAll("td")[1].textContent = workout.owner_username; //Owner - rows[3].querySelectorAll("td")[1].textContent = workout.exercise_instances.length; // Exercises + let table = aWorkout.querySelector("table"); + let rows = table.querySelectorAll("tr"); + rows[0].querySelectorAll( + "td" + )[1].textContent = localDate.toLocaleDateString(); // Date + rows[1].querySelectorAll( + "td" + )[1].textContent = localDate.toLocaleTimeString(); // Time + rows[2].querySelectorAll("td")[1].textContent = workout.owner_username; //Owner + rows[3].querySelectorAll("td")[1].textContent = + workout.exercise_instances.length; // Exercises - container.appendChild(aWorkout); - }); - return workouts; - } + container.appendChild(aWorkout); + }); + return workouts; + } } function createWorkout() { - window.location.replace("workout.html"); + window.location.replace("workout.html"); +} + +function planWorkout() { + window.location.replace("plannedWorkout.html"); } window.addEventListener("DOMContentLoaded", async () => { - let createButton = document.querySelector("#btn-create-workout"); - createButton.addEventListener("click", createWorkout); - let ordering = "-date"; - - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.has('ordering')) { - let aSort = null; - ordering = urlParams.get('ordering'); - if (ordering == "name" || ordering == "owner" || ordering == "date") { - let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); - aSort.href = `?ordering=-${ordering}`; - } - } - - let currentSort = document.querySelector("#current-sort"); - currentSort.innerHTML = (ordering.startsWith("-") ? "Descending" : "Ascending") + " " + ordering.replace("-", ""); - - let currentUser = await getCurrentUser(); - // grab username - if (ordering.includes("owner")) { - ordering += "__username"; + let createButton = document.querySelector("#btn-create-workout"); + createButton.addEventListener("click", createWorkout); + + let planButton = document.querySelector("#btn-plan-workout"); + planButton.addEventListener("click", planWorkout); + let ordering = "-date"; + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has("ordering")) { + let aSort = null; + ordering = urlParams.get("ordering"); + if (ordering == "name" || ordering == "owner" || ordering == "date") { + let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); + aSort.href = `?ordering=-${ordering}`; } - let workouts = await fetchWorkouts(ordering); - - let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); - for (let i = 0; i < tabEls.length; i++) { - let tabEl = tabEls[i]; - tabEl.addEventListener('show.bs.tab', function (event) { - let workoutAnchors = document.querySelectorAll('.workout'); - for (let j = 0; j < workouts.length; j++) { - // I'm assuming that the order of workout objects matches - // the other of the workout anchor elements. They should, given - // that I just created them. - let workout = workouts[j]; - let workoutAnchor = workoutAnchors[j]; - - switch (event.currentTarget.id) { - case "list-my-workouts-list": - if (workout.owner == currentUser.url) { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - case "list-athlete-workouts-list": - if (currentUser.athletes && currentUser.athletes.includes(workout.owner)) { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - case "list-public-workouts-list": - if (workout.visibility == "PU") { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - default : - workoutAnchor.classList.remove('hide'); - break; - } + } + + let currentSort = document.querySelector("#current-sort"); + currentSort.innerHTML = + (ordering.startsWith("-") ? "Descending" : "Ascending") + + " " + + ordering.replace("-", ""); + + let currentUser = await getCurrentUser(); + // grab username + if (ordering.includes("owner")) { + ordering += "__username"; + } + let workouts = await fetchWorkouts(ordering); + + let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); + for (let i = 0; i < tabEls.length; i++) { + let tabEl = tabEls[i]; + tabEl.addEventListener("show.bs.tab", function (event) { + let workoutAnchors = document.querySelectorAll(".workout"); + for (let j = 0; j < workouts.length; j++) { + // I'm assuming that the order of workout objects matches + // the other of the workout anchor elements. They should, given + // that I just created them. + let workout = workouts[j]; + let workoutAnchor = workoutAnchors[j]; + + switch (event.currentTarget.id) { + case "list-my-logged-workouts-list": + if (workout.owner == currentUser.url) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); } - }); - } -}); \ No newline at end of file + + if (!workout.planned) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-my-planned-workouts-list": + if (workout.owner == currentUser.url) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + + if (workout.planned) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-athlete-workouts-list": + if ( + currentUser.athletes && + currentUser.athletes.includes(workout.owner) + ) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-public-workouts-list": + if (workout.visibility == "PU") { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + default: + workoutAnchor.classList.remove("hide"); + break; + } + } + }); + } +}); diff --git a/frontend/www/workout.html b/frontend/www/workout.html index 2e5d881a2103c5117d061618f44465d4486f7e7f..849b3fa093ad8ac03b08fe95801728260af00a84 100644 --- a/frontend/www/workout.html +++ b/frontend/www/workout.html @@ -17,9 +17,14 @@ <div class="container"> <div class="row"> <div class="col-lg"> - <h3 class="mt-3">View/Edit Workout</h3> + <h3 class="mt-3">View/Edit Logged Workout</h3> </div> - </div> + </div> + <div class="row"> + <div class="col-lg"> + <p class="mt-4">A logged workout is a workout you have completed</p> + </div> + </div> <form class="row g-3 mb-4" id="form-workout"> <div class="col-lg-6 "> <label for="inputName" class="form-label">Name</label> diff --git a/frontend/www/workouts.html b/frontend/www/workouts.html index b34439d55031d7f029647d8a425669b795d8fde0..07a5c42f91513e7822acc9a93d14f88bcceea913 100644 --- a/frontend/www/workouts.html +++ b/frontend/www/workouts.html @@ -20,13 +20,15 @@ <p>Here you can view workouts completed by you, your athletes, or the public. Click on a workout to view its details.</p> <input type="button" class="btn btn-success" id="btn-create-workout" value="Log new workout"> + <input type="button" class="btn btn-success" id="btn-plan-workout" value="Plan new workout"> </div> </div> <div class="row"> <div class="col-lg text-center"> <div class="list-group list-group-horizontal d-inline-flex mt-2" id="list-tab" role="tablist"> <a class="list-group-item list-group-item-action active" id="list-all-workouts-list" data-bs-toggle="list" href="#list-all-workouts" role="tab" aria-controls="all">All Workouts</a> - <a class="list-group-item list-group-item-action" id="list-my-workouts-list" data-bs-toggle="list" href="#list-my-workouts" role="tab" aria-controls="my">My Workouts</a> + <a class="list-group-item list-group-item-action" id="list-my-logged-workouts-list" data-bs-toggle="list" href="#list-my-workouts" role="tab" aria-controls="my">My logged Workouts</a> + <a class="list-group-item list-group-item-action" id="list-my-planned-workouts-list" data-bs-toggle="list" href="#list-my-planned-workouts" role="tab" aria-controls="my">My Planned workouts</a> <a class="list-group-item list-group-item-action" id="list-athlete-workouts-list" data-bs-toggle="list" href="#list-athlete-workouts" role="tab" aria-controls="athlete">Athlete Workouts</a> <a class="list-group-item list-group-item-action" id="list-public-workouts-list" data-bs-toggle="list" href="#list-public-workouts" role="tab" aria-controls="public">Public Workouts</a> </div>