diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000000000000000000000000000000000..15a15b218a29e09c9190992732698d646e4d659a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with NO BOM" /> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc54171738be04e96ff243fa574f9fdfd287a6f0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (venv)" project-jdk-type="Python SDK" /> + <component name="PyCharmProfessionalAdvertiser"> + <option name="shown" value="true" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd76ab9e5abd475bbe7464da36a2875cd4b37ec2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/tdt4242-base.iml" filepath="$PROJECT_DIR$/.idea/tdt4242-base.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/tdt4242-base.iml b/.idea/tdt4242-base.iml new file mode 100644 index 0000000000000000000000000000000000000000..6711606311e2664bd835f92b5c114681d2e284f5 --- /dev/null +++ b/.idea/tdt4242-base.iml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="TestRunnerService"> + <option name="PROJECT_TEST_RUNNER" value="Unittests" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..94a25f7f4cb416c083d265558da75d457237d671 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000000000000000000000000000000000000..d44df01754ba002900aceb3624ca36b31bdd8fec --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,523 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ChangeListManager"> + <list default="true" id="d5050d50-3f7c-4f19-b4bf-cacb771bc0b4" name="Default Changelist" comment=""> + <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/backend/secfit/workouts/views.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/secfit/workouts/views.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/frontend/www/scripts/workouts.js" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/www/scripts/workouts.js" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/frontend/www/styles/style.css" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/www/styles/style.css" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/frontend/www/workouts.html" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/www/workouts.html" afterDir="false" /> + </list> + <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" /> + <option name="SHOW_DIALOG" value="false" /> + <option name="HIGHLIGHT_CONFLICTS" value="true" /> + <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> + <option name="LAST_RESOLUTION" value="IGNORE" /> + </component> + <component name="FileEditorManager"> + <leaf SIDE_TABS_SIZE_LIMIT_KEY="300"> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="247"> + <caret line="406" column="84" selection-start-line="406" selection-start-column="84" selection-end-line="406" selection-end-column="84" /> + <folding> + <element signature="e#88#131#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/backend/secfit/users/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-408"> + <folding> + <element signature="e#0#13#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/frontend/www/workouts.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="315"> + <caret line="57" column="27" lean-forward="true" selection-start-line="57" selection-start-column="27" selection-end-line="57" selection-end-column="27" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="true"> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/workouts.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-226"> + <caret line="149" column="26" selection-start-line="149" selection-start-column="26" selection-end-line="149" selection-end-column="26" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/exercises.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/exercise.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="39" column="75" lean-forward="true" selection-start-line="39" selection-start-column="2" selection-end-line="39" selection-end-column="75" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/frontend/www/styles/style.css"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="97"> + <caret line="74" column="21" selection-start-line="74" selection-start-column="15" selection-end-line="74" selection-end-column="21" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/backend/secfit/runtime.txt"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/backend/secfit/seed.json"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/backend/secfit/manage.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + </file> + </leaf> + </component> + <component name="FindInProjectRecents"> + <findStrings> + <find>user</find> + <find>lead</find> + <find>leader</find> + <find>isow</find> + <find>isownerofw</find> + <find>status</find> + <find>like</find> + <find>DJANGO_SUPERUSER_PASSWORD</find> + <find>User</find> + <find>post</find> + </findStrings> + </component> + <component name="Git.Settings"> + <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> + </component> + <component name="IdeDocumentHistory"> + <option name="CHANGED_PATHS"> + <list> + <option value="$PROJECT_DIR$/backend/secfit/secfit/settings.py" /> + <option value="$PROJECT_DIR$/frontend/www/scripts/exercise.js" /> + <option value="$PROJECT_DIR$/frontend/www/exercise.html" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/permissions.py" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/serializers.py" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/models.py" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/admin.py" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/urls.py" /> + <option value="$PROJECT_DIR$/frontend/www/styles/style.css" /> + <option value="$PROJECT_DIR$/frontend/www/workouts.html" /> + <option value="$PROJECT_DIR$/backend/secfit/workouts/views.py" /> + <option value="$PROJECT_DIR$/frontend/www/scripts/workouts.js" /> + </list> + </option> + </component> + <component name="ProjectFrameBounds" extendedState="6"> + <option name="x" value="-10" /> + <option name="y" value="-10" /> + <option name="width" value="1940" /> + <option name="height" value="1050" /> + </component> + <component name="ProjectView"> + <navigator proportions="" version="1"> + <foldersAlwaysOnTop value="true" /> + </navigator> + <panes> + <pane id="ProjectPane"> + <subPane> + <expand> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="backend" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="backend" type="462c0819:PsiDirectoryNode" /> + <item name="secfit" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="backend" type="462c0819:PsiDirectoryNode" /> + <item name="secfit" type="462c0819:PsiDirectoryNode" /> + <item name="users" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="backend" type="462c0819:PsiDirectoryNode" /> + <item name="secfit" type="462c0819:PsiDirectoryNode" /> + <item name="workouts" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="frontend" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="frontend" type="462c0819:PsiDirectoryNode" /> + <item name="www" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="frontend" type="462c0819:PsiDirectoryNode" /> + <item name="www" type="462c0819:PsiDirectoryNode" /> + <item name="scripts" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="tdt4242-base" type="b2602c69:ProjectViewProjectNode" /> + <item name="tdt4242-base" type="462c0819:PsiDirectoryNode" /> + <item name="frontend" type="462c0819:PsiDirectoryNode" /> + <item name="www" type="462c0819:PsiDirectoryNode" /> + <item name="styles" type="462c0819:PsiDirectoryNode" /> + </path> + </expand> + <select /> + </subPane> + </pane> + <pane id="Scope" /> + </panes> + </component> + <component name="PropertiesComponent"> + <property name="last_opened_file_path" value="$PROJECT_DIR$" /> + <property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" /> + </component> + <component name="RunDashboard"> + <option name="ruleStates"> + <list> + <RuleState> + <option name="name" value="ConfigurationTypeDashboardGroupingRule" /> + </RuleState> + <RuleState> + <option name="name" value="StatusDashboardGroupingRule" /> + </RuleState> + </list> + </option> + </component> + <component name="SvnConfiguration"> + <configuration /> + </component> + <component name="TaskManager"> + <task active="true" id="Default" summary="Default task"> + <changelist id="d5050d50-3f7c-4f19-b4bf-cacb771bc0b4" name="Default Changelist" comment="" /> + <created>1613726931624</created> + <option name="number" value="Default" /> + <option name="presentableId" value="Default" /> + <updated>1613726931624</updated> + </task> + <servers /> + </component> + <component name="ToolWindowManager"> + <frame x="-7" y="-7" width="1550" height="838" extended-state="6" /> + <layout> + <window_info content_ui="combo" id="Project" order="0" visible="true" weight="0.24983476" /> + <window_info id="Structure" order="1" side_tool="true" weight="0.25" /> + <window_info id="Favorites" order="2" side_tool="true" /> + <window_info anchor="bottom" id="Message" order="0" /> + <window_info anchor="bottom" id="Find" order="1" visible="true" weight="0.3286119" /> + <window_info anchor="bottom" id="Run" order="2" /> + <window_info anchor="bottom" id="Debug" order="3" weight="0.4" /> + <window_info anchor="bottom" id="Cvs" order="4" weight="0.25" /> + <window_info anchor="bottom" id="Inspection" order="5" weight="0.4" /> + <window_info anchor="bottom" id="TODO" order="6" /> + <window_info anchor="bottom" id="Version Control" order="7" /> + <window_info active="true" anchor="bottom" id="Terminal" order="8" visible="true" weight="0.22096318" /> + <window_info anchor="bottom" id="Event Log" order="9" side_tool="true" /> + <window_info anchor="bottom" id="Python Console" order="10" /> + <window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" /> + <window_info anchor="right" id="Ant Build" order="1" weight="0.25" /> + <window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" /> + </layout> + </component> + <component name="editorHistoryManager"> + <entry file="file://$PROJECT_DIR$/backend/secfit/secfit/asgi.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/secfit/urls.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/secfit/views.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/comments/urls.py"> + <provider selected="true" editor-type-id="text-editor"> + <state> + <folding> + <element signature="e#0#37#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/exercises.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-459" /> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/defaults.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/login.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-319" /> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/logout.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/myathletes.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/mycoach.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/navbar.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-282" /> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/register.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-13"> + <caret line="16" column="48" selection-start-line="16" selection-start-column="48" selection-end-line="16" selection-end-column="48" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/comments/permissions.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/comments/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-1600"> + <folding> + <element signature="e#0#35#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$USER_HOME$/PU/gruppe-15/apartments/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-3201"> + <caret line="71" column="44" lean-forward="true" selection-start-line="71" selection-start-column="44" selection-end-line="71" selection-end-column="44" /> + <folding> + <element signature="e#0#15#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/users/permissions.py"> + <provider selected="true" editor-type-id="text-editor"> + <state> + <folding> + <element signature="e#0#38#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/workout.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/exercise.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="272"> + <caret line="52" column="21" selection-start-line="52" selection-start-column="21" selection-end-line="52" selection-end-column="21" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/comments/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="321"> + <caret line="45" column="5" selection-start-line="43" selection-start-column="4" selection-end-line="45" selection-end-column="5" /> + <folding> + <element signature="e#0#28#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/mixins.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/tests.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/permissions.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="1037"> + <caret line="68" lean-forward="true" selection-start-line="68" selection-end-line="68" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/serializers.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="68"> + <caret line="4" column="88" lean-forward="true" selection-start-line="4" selection-start-column="88" selection-end-line="4" selection-end-column="88" /> + <folding> + <element signature="e#48#86#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/admin.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="187"> + <caret line="11" lean-forward="true" selection-start-line="11" selection-end-line="11" /> + <folding> + <element signature="e#89#121#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/urls.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="374"> + <caret line="37" column="31" selection-start-line="37" selection-start-column="31" selection-end-line="37" selection-end-column="31" /> + <folding> + <element signature="e#0#37#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="2312"> + <caret line="163" column="76" selection-start-line="163" selection-start-column="76" selection-end-line="163" selection-end-column="76" /> + <folding> + <element signature="e#225#234#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/secfit/settings.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="278"> + <caret line="143" column="8" selection-start-line="143" selection-start-column="8" selection-end-line="143" selection-end-column="8" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/users/forms.py"> + <provider selected="true" editor-type-id="text-editor"> + <state> + <folding> + <element signature="e#0#24#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/db.sqlite3"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/users/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="901"> + <caret line="55" column="67" selection-start-line="55" selection-start-column="67" selection-end-line="55" selection-end-column="67" /> + <folding> + <element signature="e#0#28#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/docker-compose.yml"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/Dockerfile"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="663"> + <caret line="39" column="4" selection-start-line="39" selection-start-column="4" selection-end-line="39" selection-end-column="4" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/Dockerfile"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/nginx.conf"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/runtime.txt"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/seed.json"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/manage.py"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/users/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-408"> + <folding> + <element signature="e#0#13#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/styles/style.css"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="97"> + <caret line="74" column="21" selection-start-line="74" selection-start-column="15" selection-end-line="74" selection-end-column="21" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/exercises.js"> + <provider selected="true" editor-type-id="text-editor" /> + </entry> + <entry file="file://$PROJECT_DIR$/backend/secfit/workouts/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="247"> + <caret line="406" column="84" selection-start-line="406" selection-start-column="84" selection-end-line="406" selection-end-column="84" /> + <folding> + <element signature="e#88#131#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/exercise.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="39" column="75" lean-forward="true" selection-start-line="39" selection-start-column="2" selection-end-line="39" selection-end-column="75" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/workouts.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="315"> + <caret line="57" column="27" lean-forward="true" selection-start-line="57" selection-start-column="27" selection-end-line="57" selection-end-column="27" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/frontend/www/scripts/workouts.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-226"> + <caret line="149" column="26" selection-start-line="149" selection-start-column="26" selection-end-line="149" selection-end-column="26" /> + </state> + </provider> + </entry> + </component> +</project> \ No newline at end of file diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 92336536fbb75beeade6bfc8e60eb393531832c9..73217533db79c786b9a0003bbb130ecfee262db4 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -136,8 +136,12 @@ MEDIA_URL = "/media/" REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", + #"DEFAULT_AUTHENTICATION_CLASSES": ( + # "rest_framework_simplejwt.authentication.JWTAuthentication", + #), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + "rest_framework_simplejwt.authentication.JWTAuthentication" ), } diff --git a/backend/secfit/workouts/admin.py b/backend/secfit/workouts/admin.py index cb43794b85492adcb933dc4e46f875029dc411cf..1c162367f5399ff3b44d36d88339157874f3a2ca 100644 --- a/backend/secfit/workouts/admin.py +++ b/backend/secfit/workouts/admin.py @@ -3,9 +3,10 @@ from django.contrib import admin # Register your models here. -from .models import Exercise, ExerciseInstance, Workout, WorkoutFile +from .models import Exercise, ExerciseInstance, Workout, WorkoutFile, WorkoutLike admin.site.register(Exercise) admin.site.register(ExerciseInstance) admin.site.register(Workout) admin.site.register(WorkoutFile) +admin.site.register(WorkoutLike) diff --git a/backend/secfit/workouts/migrations/0004_workoutlike.py b/backend/secfit/workouts/migrations/0004_workoutlike.py new file mode 100644 index 0000000000000000000000000000000000000000..a58f7e40ee98b4e15c46fb6856faa228b656b070 --- /dev/null +++ b/backend/secfit/workouts/migrations/0004_workoutlike.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1 on 2021-02-21 14:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('workouts', '0003_rememberme'), + ] + + operations = [ + migrations.CreateModel( + name='WorkoutLike', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('userLiking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userLiking', to=settings.AUTH_USER_MODEL)), + ('workoutToLike', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workouts.workout')), + ], + ), + ] diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 5e3c6d1614d54992b42491bee8207a65410c5961..170be1ab066630e95f985d0fd1e9d7579cd648ff 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -151,3 +151,15 @@ class RememberMe(models.Model): def __str__(self): return self.remember_me + + +# Likes for a workout +class WorkoutLike(models.Model): + + # The workout that is being liked + workoutToLike = models.ForeignKey(Workout, on_delete=models.CASCADE) + + # The user doing the liking + userLiking = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="userLiking" + ) \ No newline at end of file diff --git a/backend/secfit/workouts/urls.py b/backend/secfit/workouts/urls.py index 7c46a3f1ff311edc25dd455bb85780c1a1644738..fb4861d95f0cb16a5a817fc72412755afafb0895 100644 --- a/backend/secfit/workouts/urls.py +++ b/backend/secfit/workouts/urls.py @@ -27,6 +27,16 @@ urlpatterns = format_suffix_patterns( views.ExerciseInstanceList.as_view(), name="exercise-instance-list", ), + path( + "api/leaderboards/<int:pk>/", + views.Leaderboards.as_view(), + name="leaderboards", + ), + path( + "api/workoutLiking/<int:pk>/", + views.WorkoutLiking.as_view(), + name="WorkoutLiking", + ), path( "api/exercise-instances/<int:pk>/", views.ExerciseInstanceDetail.as_view(), diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf40454376b23d233f9fe2cecaf9da43fddb8..1ec5a605f0d66b475c3ad859961aae08030da73e 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -6,10 +6,12 @@ from rest_framework import permissions from rest_framework.parsers import ( JSONParser, ) +import json +from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse -from django.db.models import Q +from django.db.models import Q, Sum, F, IntegerField from rest_framework import filters from workouts.parsers import MultipartJsonParser from workouts.permissions import ( @@ -22,7 +24,7 @@ from workouts.permissions import ( IsWorkoutPublic, ) from workouts.mixins import CreateListModelMixin -from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile +from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, WorkoutLike from workouts.serializers import WorkoutSerializer, ExerciseSerializer from workouts.serializers import RememberMeSerializer from workouts.serializers import ExerciseInstanceSerializer, WorkoutFileSerializer @@ -34,6 +36,9 @@ from collections import namedtuple import base64, pickle from django.core.signing import Signer +from users.models import User +from rest_framework.views import APIView + @api_view(["GET"]) def api_root(request, format=None): @@ -204,7 +209,6 @@ class ExerciseDetail( HTTP methods: GET, PUT, PATCH, DELETE """ - queryset = Exercise.objects.all() serializer_class = ExerciseSerializer permission_classes = [permissions.IsAuthenticated] @@ -221,6 +225,43 @@ class ExerciseDetail( def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) +class Leaderboards(APIView): + + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, pk): + + # User must be logged in + if self.request.user: + + leaderboardNumbers = ExerciseInstance.objects.filter(Q(exercise__pk=pk) & Q(workout__visibility='PU')).values('workout__owner__pk').annotate(amount=Sum(F("sets") * F("number"), output_field=IntegerField())).order_by('-amount') + + leaderboardResult = [] + + # Iterates through the top 5 entries in the leaderboard and formats it correctly + for i in range(0, min(5, len(leaderboardNumbers))): + leaderboardResult.append({"name": User.objects.get(pk=leaderboardNumbers[i]['workout__owner__pk']).username, "value": leaderboardNumbers[i]['amount']}) + + # Applies the rank to the leaderboard entry; if two or more users have the score they get the same rank + if i > 0 and leaderboardNumbers[i-1]["amount"] == leaderboardNumbers[i]["amount"]: + leaderboardResult[i]["rank"] = leaderboardResult[i-1]["rank"] + else: + leaderboardResult[i]["rank"] = i+1 + + # Finds the user in the leaderboard list. If the user is not in the leaderboard list, + # the user is automatically given a score of 0 and the worst rank + + currentLoggedInUser = self.request.user + + for j in range(0, len(leaderboardNumbers)): + if leaderboardNumbers[j]['workout__owner__pk'] == currentLoggedInUser.pk: + leaderboardResult.append({"name": currentLoggedInUser.username, "value": leaderboardNumbers[j]["amount"], "rank": j+1}) + break + else: + leaderboardResult.append({"name": currentLoggedInUser.username, "value": 0, "rank": len(leaderboardNumbers) + 1}) + + return Response(leaderboardResult) + class ExerciseInstanceList( mixins.ListModelMixin, @@ -340,3 +381,35 @@ class WorkoutFileDetail( def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + +# View for fetching like amount, and for creating new likes +class WorkoutLiking(APIView): + permission_classes = [permissions.IsAuthenticated] + + # Returns a tuple with a boolean value that is true if liking is allowed (the workout does not belong to the user + # and the workout has not been liked before), and the amount of likes that the workout has + def get(self, request, pk): + + likingAllowed = Workout.objects.get(pk=pk).owner != self.request.user and WorkoutLike.objects.filter( + Q(userLiking=self.request.user) & Q(workoutToLike__pk=pk)).count() == 0 + + likeAmount = WorkoutLike.objects.filter(Q(workoutToLike__pk=pk)).count() + 1 + + return Response((likingAllowed, likeAmount), status.HTTP_200_OK) + + # Tries to like a new post and returns the same as the GET above + def post(self, request, pk): + + likingAllowed = Workout.objects.get(pk=pk).owner != self.request.user and WorkoutLike.objects.filter( + Q(userLiking=self.request.user) & Q(workoutToLike__pk=pk)).count() == 0 + + likeAmount = WorkoutLike.objects.filter(Q(workoutToLike__pk=pk)).count() + 1 + + if likingAllowed: + newWorkoutLike = WorkoutLike(workoutToLike=Workout.objects.get(pk=pk), userLiking=self.request.user) + newWorkoutLike.save() + + return Response((False, likeAmount + 1), status.HTTP_201_CREATED) + + return Response((likingAllowed, likeAmount), status.HTTP_100_CONTINUE) \ No newline at end of file diff --git a/frontend/www/exercise.html b/frontend/www/exercise.html index 741bca077b7962809c16c5db83a223f062ffb49b..28adf4dfe6f17d7e40625a653b9b6954a0ae6413 100644 --- a/frontend/www/exercise.html +++ b/frontend/www/exercise.html @@ -45,7 +45,15 @@ </div> </form> + <div class="row"> + <div class="col-lg"> + <h3 class="mt-3">Leaderboard</h3> + </div> </div> + <table id="leaderboardstable" class="table table-striped"> + <tr><th>Rank<th>Username<th>Score + </table> + </div> <script src="scripts/defaults.js"></script> <script src="scripts/scripts.js"></script> <script src="scripts/exercise.js"></script> diff --git a/frontend/www/scripts/exercise.js b/frontend/www/scripts/exercise.js index aa99f0ef40d2eeba7ffe8a7002d7dd808d984a60..c9d6a0a346cd81cb57f9aad48175f41949788bd0 100644 --- a/frontend/www/scripts/exercise.js +++ b/frontend/www/scripts/exercise.js @@ -4,151 +4,200 @@ let deleteButton; let editButton; let oldFormData; - function handleCancelButtonDuringEdit() { - setReadOnly(true, "#form-exercise"); - okButton.className += " hide"; - deleteButton.className += " hide"; - cancelButton.className += " hide"; - editButton.className = editButton.className.replace(" hide", ""); - - cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); - - let form = document.querySelector("#form-exercise"); - if (oldFormData.has("name")) form.name.value = oldFormData.get("name"); - if (oldFormData.has("description")) form.description.value = oldFormData.get("description"); - if (oldFormData.has("unit")) form.unit.value = oldFormData.get("unit"); - - oldFormData.delete("name"); - oldFormData.delete("description"); - oldFormData.delete("unit"); - + setReadOnly(true, "#form-exercise"); + okButton.className += " hide"; + deleteButton.className += " hide"; + cancelButton.className += " hide"; + editButton.className = editButton.className.replace(" hide", ""); + + cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); + + let form = document.querySelector("#form-exercise"); + if (oldFormData.has("name")) form.name.value = oldFormData.get("name"); + if (oldFormData.has("description")) + form.description.value = oldFormData.get("description"); + if (oldFormData.has("unit")) form.unit.value = oldFormData.get("unit"); + + oldFormData.delete("name"); + oldFormData.delete("description"); + oldFormData.delete("unit"); } function handleCancelButtonDuringCreate() { - window.location.replace("exercises.html"); + window.location.replace("exercises.html"); } async function createExercise() { - let form = document.querySelector("#form-exercise"); - let formData = new FormData(form); - let body = {"name": formData.get("name"), - "description": formData.get("description"), - "unit": formData.get("unit")}; - - let response = await sendRequest("POST", `${HOST}/api/exercises/`, body); - - if (response.ok) { - window.location.replace("exercises.html"); - } else { - let data = await response.json(); - let alert = createAlert("Could not create new exercise!", data); - document.body.prepend(alert); - } + let form = document.querySelector("#form-exercise"); + let formData = new FormData(form); + let body = { + name: formData.get("name"), + description: formData.get("description"), + unit: formData.get("unit"), + }; + + let response = await sendRequest("POST", `${HOST}/api/exercises/`, body); + + if (response.ok) { + window.location.replace("exercises.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new exercise!", data); + document.body.prepend(alert); + } } function handleEditExerciseButtonClick() { - setReadOnly(false, "#form-exercise"); + setReadOnly(false, "#form-exercise"); - editButton.className += " hide"; - okButton.className = okButton.className.replace(" hide", ""); - cancelButton.className = cancelButton.className.replace(" hide", ""); - deleteButton.className = deleteButton.className.replace(" hide", ""); + editButton.className += " hide"; + okButton.className = okButton.className.replace(" hide", ""); + cancelButton.className = cancelButton.className.replace(" hide", ""); + deleteButton.className = deleteButton.className.replace(" hide", ""); - cancelButton.addEventListener("click", handleCancelButtonDuringEdit); + cancelButton.addEventListener("click", handleCancelButtonDuringEdit); - let form = document.querySelector("#form-exercise"); - oldFormData = new FormData(form); + let form = document.querySelector("#form-exercise"); + oldFormData = new FormData(form); } async function deleteExercise(id) { - let response = await sendRequest("DELETE", `${HOST}/api/exercises/${id}/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert(`Could not delete exercise ${id}`, data); - document.body.prepend(alert); - } else { - window.location.replace("exercises.html"); - } + let response = await sendRequest("DELETE", `${HOST}/api/exercises/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete exercise ${id}`, data); + document.body.prepend(alert); + } else { + window.location.replace("exercises.html"); + } } async function retrieveExercise(id) { - let response = await sendRequest("GET", `${HOST}/api/exercises/${id}/`); - console.log(response.ok); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not retrieve exercise data!", data); - document.body.prepend(alert); - } else { - let exerciseData = await response.json(); - let form = document.querySelector("#form-exercise"); - 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 = exerciseData[key]; - input.value = newVal; - } + let response = await sendRequest("GET", `${HOST}/api/exercises/${id}/`); + console.log(response.ok); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve exercise data!", data); + document.body.prepend(alert); + } else { + let exerciseData = await response.json(); + let form = document.querySelector("#form-exercise"); + 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 = exerciseData[key]; + input.value = newVal; } + } } async function updateExercise(id) { - let form = document.querySelector("#form-exercise"); - let formData = new FormData(form); - let body = {"name": formData.get("name"), - "description": formData.get("description"), - "unit": formData.get("unit")}; - let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body); - - if (!response.ok) { - let data = await response.json(); - let alert = createAlert(`Could not update exercise ${id}`, data); - document.body.prepend(alert); - } else { - // duplicate code from handleCancelButtonDuringEdit - // you should refactor this - setReadOnly(true, "#form-exercise"); - okButton.className += " hide"; - deleteButton.className += " hide"; - cancelButton.className += " hide"; - editButton.className = editButton.className.replace(" hide", ""); - - cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); - - oldFormData.delete("name"); - oldFormData.delete("description"); - oldFormData.delete("unit"); + let form = document.querySelector("#form-exercise"); + let formData = new FormData(form); + let body = { + name: formData.get("name"), + description: formData.get("description"), + unit: formData.get("unit"), + }; + let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body); + + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not update exercise ${id}`, data); + document.body.prepend(alert); + } else { + // duplicate code from handleCancelButtonDuringEdit + // you should refactor this + setReadOnly(true, "#form-exercise"); + okButton.className += " hide"; + deleteButton.className += " hide"; + cancelButton.className += " hide"; + editButton.className = editButton.className.replace(" hide", ""); + + cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); + + oldFormData.delete("name"); + oldFormData.delete("description"); + oldFormData.delete("unit"); + } +} + +async function fetchLeaderBoards(id) { + + // Fetches leaderboard data + let response = await sendRequest("GET", `${HOST}/api/leaderboards/${id}/`); + let data = await response.json(); + + if (response.ok) { + let table = document.getElementById("leaderboardstable"); + let row, cell; + + //The user's own score will always be placed last in the JSON response + let userIndex = data.length - 1; + + for (let i = 0; i < data.length - 1; i++) { + row = table.insertRow(); + cell = row.insertCell(); + cell.textContent = data[i].rank; + cell = row.insertCell(); + cell.textContent = data[i].name; + cell = row.insertCell(); + cell.textContent = data[i].value; } + //If the user is not in top 5, the users score will also be rendered + if (data[userIndex].rank > 5) { + row = table.insertRow(); + cell = row.insertCell(); + cell.textContent = data[userIndex].rank; + cell = row.insertCell(); + cell.textContent = data[userIndex].name; + cell = row.insertCell(); + cell.textContent = data[userIndex].value; + } + } + + return data; } window.addEventListener("DOMContentLoaded", async () => { - cancelButton = document.querySelector("#btn-cancel-exercise"); - okButton = document.querySelector("#btn-ok-exercise"); - deleteButton = document.querySelector("#btn-delete-exercise"); - editButton = document.querySelector("#btn-edit-exercise"); - oldFormData = null; - - const urlParams = new URLSearchParams(window.location.search); - - // view/edit - if (urlParams.has('id')) { - const exerciseId = urlParams.get('id'); - await retrieveExercise(exerciseId); - - editButton.addEventListener("click", handleEditExerciseButtonClick); - deleteButton.addEventListener("click", (async (id) => await deleteExercise(id)).bind(undefined, exerciseId)); - okButton.addEventListener("click", (async (id) => await updateExercise(id)).bind(undefined, exerciseId)); - } - //create - else { - setReadOnly(false, "#form-exercise"); - - editButton.className += " hide"; - okButton.className = okButton.className.replace(" hide", ""); - cancelButton.className = cancelButton.className.replace(" hide", ""); - - okButton.addEventListener("click", async () => await createExercise()); - cancelButton.addEventListener("click", handleCancelButtonDuringCreate); - } -}); \ No newline at end of file + cancelButton = document.querySelector("#btn-cancel-exercise"); + okButton = document.querySelector("#btn-ok-exercise"); + deleteButton = document.querySelector("#btn-delete-exercise"); + editButton = document.querySelector("#btn-edit-exercise"); + oldFormData = null; + + const urlParams = new URLSearchParams(window.location.search); + + // view/edit + if (urlParams.has("id")) { + const exerciseId = urlParams.get("id"); + await retrieveExercise(exerciseId); + await fetchLeaderBoards(exerciseId); + + editButton.addEventListener("click", handleEditExerciseButtonClick); + deleteButton.addEventListener( + "click", + (async (id) => await deleteExercise(id)).bind(undefined, exerciseId) + ); + okButton.addEventListener( + "click", + (async (id) => await updateExercise(id)).bind(undefined, exerciseId) + ); + } + //create + else { + setReadOnly(false, "#form-exercise"); + + editButton.className += " hide"; + okButton.className = okButton.className.replace(" hide", ""); + cancelButton.className = cancelButton.className.replace(" hide", ""); + + okButton.addEventListener("click", async () => await createExercise()); + cancelButton.addEventListener("click", handleCancelButtonDuringCreate); + } + + +}); diff --git a/frontend/www/scripts/workouts.js b/frontend/www/scripts/workouts.js index 772be1ea070499ad9574d787d990c1ca17097bdf..1d8ecc639dcb3da15a3efd9ab301caf7b0a560d8 100644 --- a/frontend/www/scripts/workouts.js +++ b/frontend/www/scripts/workouts.js @@ -8,7 +8,17 @@ async function fetchWorkouts(ordering) { let workouts = data.results; let container = document.getElementById('div-content'); - workouts.forEach(workout => { + + for(const workout of workouts){ + + let workoutLikes = await sendRequest("GET", `${HOST}/api/workoutLiking/${workout.id}`); + + let workoutLikesData = [true, 1] + + if(workoutLikes.ok){ + workoutLikesData = await workoutLikes.json() + } + let templateWorkout = document.querySelector("#template-workout"); let cloneWorkout = templateWorkout.content.cloneNode(true); @@ -27,8 +37,35 @@ async function fetchWorkouts(ordering) { rows[2].querySelectorAll("td")[1].textContent = workout.owner_username; //Owner rows[3].querySelectorAll("td")[1].textContent = workout.exercise_instances.length; // Exercises + rows[4].querySelectorAll("td")[1].textContent = workoutLikesData[1] + + let likeButton = rows[4].querySelectorAll("td")[2].querySelector(".like-button") + + if(!workoutLikesData[0]){ + likeButton.classList.add("active") + } + + likeButton.addEventListener("click", async function(e) { + e.preventDefault(); + + if(!this.classList.contains("active")){ + this.classList.add("active"); + this.classList.add("animated"); + generateClones(this); + + let likeWorkoutResponse = await sendRequest("POST", `${HOST}/api/workoutLiking/${workout.id}/`, {}); + + if(likeWorkoutResponse.ok){ + let likeWorkoutData = await likeWorkoutResponse.json() + rows[4].querySelectorAll("td")[1].textContent = likeWorkoutData[1] + likeButton.classList.add("active") + } + } + }) + + container.appendChild(aWorkout); - }); + }; return workouts; } } @@ -103,4 +140,48 @@ window.addEventListener("DOMContentLoaded", async () => { } }); } -}); \ No newline at end of file +}); + + + + + +function generateClones(button) { + let clones = randomInt(4, 7); + for (let it = 1; it <= clones; it++) { + let clone = button.querySelector("svg").cloneNode(true), + size = randomInt(5, 16); + button.appendChild(clone); + clone.setAttribute("width", size); + clone.setAttribute("height", size); + clone.style.position = "absolute"; + clone.style.transition = + "transform 0.5s cubic-bezier(0.12, 0.74, 0.58, 0.99) 0.3s, opacity 1s ease-out .5s"; + let animTimeout = setTimeout(function() { + clearTimeout(animTimeout); + clone.style.transform = + "translate3d(" + + (plusOrMinus() * randomInt(10, 25)) + + "px," + + (plusOrMinus() * randomInt(10, 25)) + + "px,0)"; + clone.style.opacity = 0; + }, 1); + let removeNodeTimeout = setTimeout(function() { + clone.parentNode.removeChild(clone); + clearTimeout(removeNodeTimeout); + }, 900); + let removeClassTimeout = setTimeout( function() { + button.classList.remove("animated") + }, 600); + } +} + + +function plusOrMinus() { + return Math.random() < 0.5 ? -1 : 1; +} + +function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} diff --git a/frontend/www/styles/style.css b/frontend/www/styles/style.css index 066705ce965bb162c61ebaf65ff77b9a0824eaf3..ed891107d13ca9b42e3e24acc1c4219285f62567 100644 --- a/frontend/www/styles/style.css +++ b/frontend/www/styles/style.css @@ -62,3 +62,96 @@ .link-block { display: block; } + + + + +/* +Like button example from https://codepen.io/abaicus/pen/gNXdQP/ +*/ + +.like-button { + display: flex; + align-items: center; + justify-content: center; +} +.like-button.animated { + -webkit-animation: pop 0.9s both; + animation: pop 0.9s both; +} +.like-button svg { + opacity: 1; +} +.like-button svg path { + fill: #333; + transition: fill .4s ease-out; +} +.like-button.active svg path { + fill: #2196f3; + } + +.like-button.active { + pointer-events: none; +} + +@-webkit-keyframes pop { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pop { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} \ No newline at end of file diff --git a/frontend/www/workouts.html b/frontend/www/workouts.html index b34439d55031d7f029647d8a425669b795d8fde0..79d149a569856052b6fcd481134e62e0c5d59a64 100644 --- a/frontend/www/workouts.html +++ b/frontend/www/workouts.html @@ -50,6 +50,18 @@ <tr><td>Time:</td><td></td></tr> <tr><td>Owner:</td><td></td></tr> <tr><td>Exercises:</td><td></td></tr> + <tr> + <td> + Likes: + </td> + <td></td> + <td> + <a href="#" class="like-button"> + <?xml version="1.0" encoding="utf-8"?> + <svg width="20" height="20" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M320 1344q0-26-19-45t-45-19q-27 0-45.5 19t-18.5 45q0 27 18.5 45.5t45.5 18.5q26 0 45-18.5t19-45.5zm160-512v640q0 26-19 45t-45 19h-288q-26 0-45-19t-19-45v-640q0-26 19-45t45-19h288q26 0 45 19t19 45zm1184 0q0 86-55 149 15 44 15 76 3 76-43 137 17 56 0 117-15 57-54 94 9 112-49 181-64 76-197 78h-129q-66 0-144-15.5t-121.5-29-120.5-39.5q-123-43-158-44-26-1-45-19.5t-19-44.5v-641q0-25 18-43.5t43-20.5q24-2 76-59t101-121q68-87 101-120 18-18 31-48t17.5-48.5 13.5-60.5q7-39 12.5-61t19.5-52 34-50q19-19 45-19 46 0 82.5 10.5t60 26 40 40.5 24 45 12 50 5 45 .5 39q0 38-9.5 76t-19 60-27.5 56q-3 6-10 18t-11 22-8 24h277q78 0 135 57t57 135z"/></svg> + </a> + </td> + </tr> </table> </div> </a>