From a2684067cf30f15d6eeb59cf44d7cdd7ccda979d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sigurd=20R=C3=B8stad=20Augdal?= <sigurdra@stud.ntnu.no>
Date: Wed, 3 Mar 2021 21:31:49 +0100
Subject: [PATCH] First real commit. Ref #1 #2 #4 #5 #7 #8 Long list of
 changes: Add Mealtype and Diet to navbar Add Mealtype and Diet pages Add
 Mealtype and Diet in backend Add Mealtype and Diet in db

---
 backend/secfit/diets/__init__.py              |   0
 backend/secfit/diets/admin.py                 |  11 +
 backend/secfit/diets/apps.py                  |  13 +
 .../secfit/diets/migrations/0001_initial.py   | 136 +++++++
 .../migrations/0002_auto_20200910_0222.py     |  25 ++
 .../diets/migrations/0003_rememberme.py       |  20 +
 backend/secfit/diets/migrations/__init__.py   |   0
 backend/secfit/diets/mixins.py                |  26 ++
 backend/secfit/diets/models.py                | 151 ++++++++
 backend/secfit/diets/parsers.py               |  38 ++
 backend/secfit/diets/permissions.py           |  68 ++++
 backend/secfit/diets/serializers.py           | 230 ++++++++++++
 backend/secfit/diets/tests.py                 |   6 +
 backend/secfit/diets/urls.py                  |  52 +++
 backend/secfit/diets/views.py                 | 342 +++++++++++++++++
 backend/secfit/secfit/settings.py             |   1 +
 backend/secfit/secfit/urls.py                 |   1 +
 frontend/www/diet.html                        | 129 +++++++
 frontend/www/diets.html                       |  63 ++++
 frontend/www/mealtype.html                    |  54 +++
 frontend/www/mealtypes.html                   |  47 +++
 frontend/www/scripts/diet.js                  | 353 ++++++++++++++++++
 frontend/www/scripts/diets.js                 | 106 ++++++
 frontend/www/scripts/mealtype.js              | 156 ++++++++
 frontend/www/scripts/mealtypes.js             |  42 +++
 frontend/www/scripts/navbar.js                |   2 +
 frontend/www/scripts/scripts.js               |   6 +
 27 files changed, 2078 insertions(+)
 create mode 100644 backend/secfit/diets/__init__.py
 create mode 100644 backend/secfit/diets/admin.py
 create mode 100644 backend/secfit/diets/apps.py
 create mode 100644 backend/secfit/diets/migrations/0001_initial.py
 create mode 100644 backend/secfit/diets/migrations/0002_auto_20200910_0222.py
 create mode 100644 backend/secfit/diets/migrations/0003_rememberme.py
 create mode 100644 backend/secfit/diets/migrations/__init__.py
 create mode 100644 backend/secfit/diets/mixins.py
 create mode 100644 backend/secfit/diets/models.py
 create mode 100644 backend/secfit/diets/parsers.py
 create mode 100644 backend/secfit/diets/permissions.py
 create mode 100644 backend/secfit/diets/serializers.py
 create mode 100644 backend/secfit/diets/tests.py
 create mode 100644 backend/secfit/diets/urls.py
 create mode 100644 backend/secfit/diets/views.py
 create mode 100644 frontend/www/diet.html
 create mode 100644 frontend/www/diets.html
 create mode 100644 frontend/www/mealtype.html
 create mode 100644 frontend/www/mealtypes.html
 create mode 100644 frontend/www/scripts/diet.js
 create mode 100644 frontend/www/scripts/diets.js
 create mode 100644 frontend/www/scripts/mealtype.js
 create mode 100644 frontend/www/scripts/mealtypes.js

diff --git a/backend/secfit/diets/__init__.py b/backend/secfit/diets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/secfit/diets/admin.py b/backend/secfit/diets/admin.py
new file mode 100644
index 0000000..48f90a9
--- /dev/null
+++ b/backend/secfit/diets/admin.py
@@ -0,0 +1,11 @@
+"""Module for registering models from workouts app to admin page so that they appear
+"""
+from django.contrib import admin
+
+# Register your models here.
+from .models import Mealtype, MealtypeInstance, Diet, DietFile
+
+admin.site.register(Mealtype)
+admin.site.register(MealtypeInstance)
+admin.site.register(Diet)
+admin.site.register(DietFile)
diff --git a/backend/secfit/diets/apps.py b/backend/secfit/diets/apps.py
new file mode 100644
index 0000000..1c9b2c8
--- /dev/null
+++ b/backend/secfit/diets/apps.py
@@ -0,0 +1,13 @@
+"""AppConfig for diets app
+"""
+from django.apps import AppConfig
+
+
+class DietsConfig(AppConfig):
+    """AppConfig for diets app
+
+    Attributes:
+        name (str): The name of the application
+    """
+
+    name = "diets"
diff --git a/backend/secfit/diets/migrations/0001_initial.py b/backend/secfit/diets/migrations/0001_initial.py
new file mode 100644
index 0000000..5fc875c
--- /dev/null
+++ b/backend/secfit/diets/migrations/0001_initial.py
@@ -0,0 +1,136 @@
+# Generated by Django 3.1 on 2020-08-21 03:42
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import diets.models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Mealtype",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=100)),
+                ("description", models.TextField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name="Diet",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=100)),
+                ("date", models.DateTimeField()),
+                ("notes", models.TextField()),
+                (
+                    "visibility",
+                    models.CharField(
+                        choices=[("PU", "Public"), ("CO", "Coach"), ("PR", "Private")],
+                        default="CO",
+                        max_length=2,
+                    ),
+                ),
+                (
+                    "owner",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="diets",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "ordering": ["-date"],
+            },
+        ),
+        migrations.CreateModel(
+            name="DietFile",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "file",
+                    models.FileField(upload_to=diets.models.diet_directory_path),
+                ),
+                (
+                    "owner",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="files",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    "diet",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="files",
+                        to="diets.diet",
+                    ),
+                ),
+            ],
+        ),
+        migrations.CreateModel(
+            name="MealtypeInstance",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("sets", models.IntegerField()),
+                ("number", models.IntegerField()),
+                (
+                    "mealtype",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="instances",
+                        to="diets.mealtype",
+                    ),
+                ),
+                (
+                    "diet",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="mealtype_instances",
+                        to="diets.diet",
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/backend/secfit/diets/migrations/0002_auto_20200910_0222.py b/backend/secfit/diets/migrations/0002_auto_20200910_0222.py
new file mode 100644
index 0000000..0c4ea29
--- /dev/null
+++ b/backend/secfit/diets/migrations/0002_auto_20200910_0222.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1 on 2020-09-10 00:22
+
+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),
+        ("diets", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="dietfile",
+            name="owner",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="diet_files",
+                to=settings.AUTH_USER_MODEL,
+            ),
+        ),
+    ]
diff --git a/backend/secfit/diets/migrations/0003_rememberme.py b/backend/secfit/diets/migrations/0003_rememberme.py
new file mode 100644
index 0000000..15b7732
--- /dev/null
+++ b/backend/secfit/diets/migrations/0003_rememberme.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.1 on 2021-02-04 10:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('diets', '0002_auto_20200910_0222'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='RememberMe',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('remember_me', models.CharField(max_length=500)),
+            ],
+        ),
+    ]
diff --git a/backend/secfit/diets/migrations/__init__.py b/backend/secfit/diets/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/secfit/diets/mixins.py b/backend/secfit/diets/mixins.py
new file mode 100644
index 0000000..df6cf7e
--- /dev/null
+++ b/backend/secfit/diets/mixins.py
@@ -0,0 +1,26 @@
+"""
+Mixins for the diets application
+"""
+
+
+class CreateListModelMixin(object):
+    """Mixin that allows to create multiple objects from lists.
+    Taken from https://stackoverflow.com/a/48885641
+    """
+
+    def get_serializer(self, *args, **kwargs):
+        """If an array is passed, set serializer to many.
+
+        kwargs["many"] will be set to true if an array is passed. This argument
+        is passed when retrieving the serializer.
+
+        Args:
+            *args: Variable length argument list passed to the serializer.
+            **kwargs: Arbitrary keyword arguments passed to the serializer, including "many".
+
+        Returns:
+            [type]: [description]
+        """
+        if isinstance(kwargs.get("data", {}), list):
+            kwargs["many"] = True
+        return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
diff --git a/backend/secfit/diets/models.py b/backend/secfit/diets/models.py
new file mode 100644
index 0000000..67decd7
--- /dev/null
+++ b/backend/secfit/diets/models.py
@@ -0,0 +1,151 @@
+"""Contains the models for the diets Django application. Users
+log diets (Diet), which contain instances (MealtypeInstance) of various
+type of mealtypes (Mealtype). The user can also upload files (DietFile) .
+"""
+import os
+from django.db import models
+from django.core.files.storage import FileSystemStorage
+from django.conf import settings
+from django.contrib.auth import get_user_model
+
+
+class OverwriteStorage(FileSystemStorage):
+    """Filesystem storage for overwriting files. Currently unused."""
+
+    def get_available_name(self, name, max_length=None):
+        """https://djangosnippets.org/snippets/976/
+        Returns a filename that's free on the target storage system, and
+        available for new content to be written to.
+
+        Args:
+            name (str): Name of the file
+            max_length (int, optional): Maximum length of a file name. Defaults to None.
+        """
+        if self.exists(name):
+            os.remove(os.path.join(settings.MEDIA_ROOT, name))
+
+
+# Create your models here.
+class Diet(models.Model):
+    """Django model for a diet that users can log.
+
+    A diet has several attributes, and is associated with one or more mealtypes
+    (instances) and, optionally, files uploaded by the user.
+
+    Attributes:
+        name:        Name of the diet
+        date:        Date the diet was performed or is planned
+        notes:       Notes about the diet
+        owner:       User that logged the diet
+        visibility:  The visibility level of the diet: Public, Coach, or Private
+    """
+
+    name = models.CharField(max_length=100)
+    date = models.DateTimeField()
+    notes = models.TextField()
+    owner = models.ForeignKey(
+        get_user_model(), on_delete=models.CASCADE, related_name="diets"
+    )
+
+    # Visibility levels
+    PUBLIC = "PU"  # Visible to all authenticated users
+    COACH = "CO"  # Visible only to owner and their coach
+    PRIVATE = "PR"  # Visible only to owner
+    VISIBILITY_CHOICES = [
+        (PUBLIC, "Public"),
+        (COACH, "Coach"),
+        (PRIVATE, "Private"),
+    ]  # Choices for visibility level
+
+    visibility = models.CharField(
+        max_length=2, choices=VISIBILITY_CHOICES, default=COACH
+    )
+
+    class Meta:
+        ordering = ["-date"]
+
+    def __str__(self):
+        return self.name
+
+
+class Mealtype(models.Model):
+    """Django model for an mealtype type that users can create.
+
+    Each mealtype instance must have an mealtype type, e.g., Pushups, Crunches, or Lunges.
+
+    Attributes:
+        name:        Name of the mealtype type
+        description: Description of the mealtype type
+    """
+
+    name = models.CharField(max_length=100)
+    description = models.TextField()
+
+    def __str__(self):
+        return self.name
+
+
+class MealtypeInstance(models.Model):
+    """Django model for an instance of an mealtype.
+
+    Each diet has one or more mealtype instances, each of a given type. For example,
+    Kyle's diet on 15.06.2029 had one mealtype instance: 3 (sets) reps (unit) of
+    10 (number) pushups (mealtype type)
+
+    Attributes:
+        diet:    The diet associated with this mealtype instance
+        mealtype:   The mealtype type of this instance
+        sets:       The number of sets the owner will perform/performed
+        number:     The number of repetitions in each set the owner will perform/performed
+    """
+
+    diet = models.ForeignKey(
+        Diet, on_delete=models.CASCADE, related_name="mealtype_instances"
+    )
+    mealtype = models.ForeignKey(
+        Mealtype, on_delete=models.CASCADE, related_name="instances"
+    )
+    sets = models.IntegerField()
+    number = models.IntegerField()
+
+
+def diet_directory_path(instance, filename):
+    """Return path for which diet files should be uploaded on the web server
+
+    Args:
+        instance (DietFile): DietFile instance
+        filename (str): Name of the file
+
+    Returns:
+        str: Path where diet file is stored
+    """
+    return f"diets/{instance.diet.id}/{filename}"
+
+
+class DietFile(models.Model):
+    """Django model for file associated with a diet. Basically a wrapper.
+
+    Attributes:
+        diet: The diet for which this file has been uploaded
+        owner:   The user who uploaded the file
+        file:    The actual file that's being uploaded
+    """
+
+    diet = models.ForeignKey(Diet, on_delete=models.CASCADE, related_name="files")
+    owner = models.ForeignKey(
+        get_user_model(), on_delete=models.CASCADE, related_name="diet_files"
+    )
+    file = models.FileField(upload_to=diet_directory_path)
+
+
+class RememberMe(models.Model):
+    """Django model for an remember_me cookie used for remember me functionality.
+
+    Attributes:
+        remember_me:        Value of cookie used for remember me
+    """
+
+    remember_me = models.CharField(max_length=500)
+
+    def __str__(self):
+        return self.remember_me
diff --git a/backend/secfit/diets/parsers.py b/backend/secfit/diets/parsers.py
new file mode 100644
index 0000000..cf33375
--- /dev/null
+++ b/backend/secfit/diets/parsers.py
@@ -0,0 +1,38 @@
+"""Contains custom parsers for serializers from the diets Django app
+"""
+import json
+from rest_framework import parsers
+
+# Thanks to https://stackoverflow.com/a/50514630
+class MultipartJsonParser(parsers.MultiPartParser):
+    """Parser for serializing multipart data containing both files and JSON.
+
+    This is currently unused.
+    """
+
+    def parse(self, stream, media_type=None, parser_context=None):
+        result = super().parse(
+            stream, media_type=media_type, parser_context=parser_context
+        )
+        data = {}
+        new_files = {"files": []}
+
+        # for case1 with nested serializers
+        # parse each field with json
+        for key, value in result.data.items():
+            if not isinstance(value, str):
+                data[key] = value
+                continue
+            if "{" in value or "[" in value:
+                try:
+                    data[key] = json.loads(value)
+                except ValueError:
+                    data[key] = value
+            else:
+                data[key] = value
+
+        files = result.files.getlist("files")
+        for file in files:
+            new_files["files"].append({"file": file})
+
+        return parsers.DataAndFiles(data, new_files)
diff --git a/backend/secfit/diets/permissions.py b/backend/secfit/diets/permissions.py
new file mode 100644
index 0000000..6b2c4a8
--- /dev/null
+++ b/backend/secfit/diets/permissions.py
@@ -0,0 +1,68 @@
+"""Contains custom DRF permissions classes for the diets app
+"""
+from rest_framework import permissions
+from diets.models import Diet
+
+
+class IsOwner(permissions.BasePermission):
+    """Checks whether the requesting user is also the owner of the existing object"""
+
+    def has_object_permission(self, request, view, obj):
+        return obj.owner == request.user
+
+
+class IsOwnerOfDiet(permissions.BasePermission):
+    """Checks whether the requesting user is also the owner of the new or existing object"""
+
+    def has_permission(self, request, view):
+        if request.method == "POST":
+            if request.data.get("diet"):
+                diet_id = request.data["diet"].split("/")[-2]
+                diet = Diet.objects.get(pk=diet_id)
+                if diet:
+                    return diet.owner == request.user
+            return False
+
+        return True
+
+    def has_object_permission(self, request, view, obj):
+        return obj.diet.owner == request.user
+
+
+class IsCoachAndVisibleToCoach(permissions.BasePermission):
+    """Checks whether the requesting user is the existing object's owner's coach
+    and whether the object (diet) has a visibility of Public or Coach.
+    """
+
+    def has_object_permission(self, request, view, obj):
+        return obj.owner.coach == request.user
+
+
+class IsCoachOfDietAndVisibleToCoach(permissions.BasePermission):
+    """Checks whether the requesting user is the existing diet's owner's coach
+    and whether the object has a visibility of Public or Coach.
+    """
+
+    def has_object_permission(self, request, view, obj):
+        return obj.diet.owner.coach == request.user
+
+
+class IsPublic(permissions.BasePermission):
+    """Checks whether the object (diet) has visibility of Public."""
+
+    def has_object_permission(self, request, view, obj):
+        return obj.visibility == "PU"
+
+
+class IsDietPublic(permissions.BasePermission):
+    """Checks whether the object's diet has visibility of Public."""
+
+    def has_object_permission(self, request, view, obj):
+        return obj.diet.visibility == "PU"
+
+
+class IsReadOnly(permissions.BasePermission):
+    """Checks whether the HTTP request verb is only for retrieving data (GET, HEAD, OPTIONS)"""
+
+    def has_object_permission(self, request, view, obj):
+        return request.method in permissions.SAFE_METHODS
diff --git a/backend/secfit/diets/serializers.py b/backend/secfit/diets/serializers.py
new file mode 100644
index 0000000..39a0e7a
--- /dev/null
+++ b/backend/secfit/diets/serializers.py
@@ -0,0 +1,230 @@
+"""Serializers for the diets application
+"""
+from rest_framework import serializers
+from rest_framework.serializers import HyperlinkedRelatedField
+from diets.models import Diet, Mealtype, MealtypeInstance, DietFile, RememberMe
+
+
+class MealtypeInstanceSerializer(serializers.HyperlinkedModelSerializer):
+    """Serializer for an MealtypeInstance. Hyperlinks are used for relationships by default.
+
+    Serialized fields: url, id, mealtype, sets, number, diet
+
+    Attributes:
+        diet:    The associated diet for this instance, represented by a hyperlink
+    """
+
+    diet = HyperlinkedRelatedField(
+        queryset=Diet.objects.all(), view_name="diet-detail", required=False
+    )
+
+    class Meta:
+        model = MealtypeInstance
+        fields = ["url", "id", "mealtype", "sets", "number", "diet"]
+
+
+class DietFileSerializer(serializers.HyperlinkedModelSerializer):
+    """Serializer for a DietFile. Hyperlinks are used for relationships by default.
+
+    Serialized fields: url, id, owner, file, diet
+
+    Attributes:
+        owner:      The owner (User) of the DietFile, represented by a username. ReadOnly
+        diet:    The associate diet for this DietFile, represented by a hyperlink
+    """
+
+    owner = serializers.ReadOnlyField(source="owner.username")
+    diet = HyperlinkedRelatedField(
+        queryset=Diet.objects.all(), view_name="diet-detail", required=False
+    )
+
+    class Meta:
+        model = DietFile
+        fields = ["url", "id", "owner", "file", "diet"]
+
+    def create(self, validated_data):
+        return DietFile.objects.create(**validated_data)
+
+
+class DietSerializer(serializers.HyperlinkedModelSerializer):
+    """Serializer for a Diet. Hyperlinks are used for relationships by default.
+
+    This serializer specifies nested serialization since a diet consists of DietFiles
+    and MealtypeInstances.
+
+    Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity,
+                       mealtype_instances, files
+
+    Attributes:
+        owner_username:     Username of the owning User
+        mealtype_instance:  Serializer for ExericseInstances
+        files:              Serializer for DietFiles
+    """
+
+    owner_username = serializers.SerializerMethodField()
+    mealtype_instances = MealtypeInstanceSerializer(many=True, required=True)
+    files = DietFileSerializer(many=True, required=False)
+
+    class Meta:
+        model = Diet
+        fields = [
+            "url",
+            "id",
+            "name",
+            "date",
+            "notes",
+            "owner",
+            "owner_username",
+            "visibility",
+            "mealtype_instances",
+            "files",
+        ]
+        extra_kwargs = {"owner": {"read_only": True}}
+
+    def create(self, validated_data):
+        """Custom logic for creating MealtypeInstances, DietFiles, and a Diet.
+
+        This is needed to iterate over the files and mealtype instances, since this serializer is
+        nested.
+
+        Args:
+            validated_data: Validated files and mealtype_instances
+
+        Returns:
+            Diet: A newly created Diet
+        """
+        mealtype_instances_data = validated_data.pop("mealtype_instances")
+        files_data = []
+        if "files" in validated_data:
+            files_data = validated_data.pop("files")
+
+        diet = Diet.objects.create(**validated_data)
+
+        for mealtype_instance_data in mealtype_instances_data:
+            MealtypeInstance.objects.create(diet=diet, **mealtype_instance_data)
+        for file_data in files_data:
+            DietFile.objects.create(
+                diet=diet, owner=diet.owner, file=file_data.get("file")
+            )
+
+        return diet
+
+    def update(self, instance, validated_data):
+        """Custom logic for updating a Diet with its MealtypeInstances and Diets.
+
+        This is needed because each object in both mealtype_instances and files must be iterated
+        over and handled individually.
+
+        Args:
+            instance (Diet): Current Diet object
+            validated_data: Contains data for validated fields
+
+        Returns:
+            Diet: Updated Diet instance
+        """
+        mealtype_instances_data = validated_data.pop("mealtype_instances")
+        mealtype_instances = instance.mealtype_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.date = validated_data.get("date", instance.date)
+        instance.save()
+
+        # Handle MealtypeInstances
+
+        # This updates existing mealtype instances without adding or deleting object.
+        # zip() will yield n 2-tuples, where n is
+        # min(len(mealtype_instance), len(mealtype_instance_data))
+        for mealtype_instance, mealtype_instance_data in zip(
+            mealtype_instances.all(), mealtype_instances_data
+        ):
+            mealtype_instance.mealtype = mealtype_instance_data.get(
+                "mealtype", mealtype_instance.mealtype
+            )
+            mealtype_instance.number = mealtype_instance_data.get(
+                "number", mealtype_instance.number
+            )
+            mealtype_instance.sets = mealtype_instance_data.get(
+                "sets", mealtype_instance.sets
+            )
+            mealtype_instance.save()
+
+        # If new mealtype instances have been added to the diet, then create them
+        if len(mealtype_instances_data) > len(mealtype_instances.all()):
+            for i in range(len(mealtype_instances.all()), len(mealtype_instances_data)):
+                mealtype_instance_data = mealtype_instances_data[i]
+                MealtypeInstance.objects.create(
+                    diet=instance, **mealtype_instance_data
+                )
+        # Else if mealtype instances have been removed from the diet, then delete them
+        elif len(mealtype_instances_data) < len(mealtype_instances.all()):
+            for i in range(len(mealtype_instances_data), len(mealtype_instances.all())):
+                mealtype_instances.all()[i].delete()
+
+        # Handle DietFiles
+
+        if "files" in validated_data:
+            files_data = validated_data.pop("files")
+            files = instance.files
+
+            for file, file_data in zip(files.all(), files_data):
+                file.file = file_data.get("file", file.file)
+
+            # If new files have been added, creating new DietFiles
+            if len(files_data) > len(files.all()):
+                for i in range(len(files.all()), len(files_data)):
+                    DietFile.objects.create(
+                        diet=instance,
+                        owner=instance.owner,
+                        file=files_data[i].get("file"),
+                    )
+            # Else if files have been removed, delete DietFiles
+            elif len(files_data) < len(files.all()):
+                for i in range(len(files_data), len(files.all())):
+                    files.all()[i].delete()
+
+        return instance
+
+    def get_owner_username(self, obj):
+        """Returns the owning user's username
+
+        Args:
+            obj (Diet): Current Diet
+
+        Returns:
+            str: Username of owner
+        """
+        return obj.owner.username
+
+
+class MealtypeSerializer(serializers.HyperlinkedModelSerializer):
+    """Serializer for an Mealtype. Hyperlinks are used for relationships by default.
+
+    Serialized fields: url, id, name, description, instances
+
+    Attributes:
+        instances:  Associated mealtype instances with this Mealtype type. Hyperlinks.
+    """
+
+    instances = serializers.HyperlinkedRelatedField(
+        many=True, view_name="mealtypeinstance-detail", read_only=True
+    )
+
+    class Meta:
+        model = Mealtype
+        fields = ["url", "id", "name", "description", "instances"]
+
+
+class RememberMeSerializer(serializers.HyperlinkedModelSerializer):
+    """Serializer for an RememberMe. Hyperlinks are used for relationships by default.
+
+    Serialized fields: remember_me
+
+    Attributes:
+        remember_me:    Value of cookie used for remember me functionality
+    """
+
+    class Meta:
+        model = RememberMe
+        fields = ["remember_me"]
diff --git a/backend/secfit/diets/tests.py b/backend/secfit/diets/tests.py
new file mode 100644
index 0000000..7fbbf78
--- /dev/null
+++ b/backend/secfit/diets/tests.py
@@ -0,0 +1,6 @@
+"""
+Tests for the workouts application.
+"""
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/secfit/diets/urls.py b/backend/secfit/diets/urls.py
new file mode 100644
index 0000000..05f0b7a
--- /dev/null
+++ b/backend/secfit/diets/urls.py
@@ -0,0 +1,52 @@
+from django.urls import path, include
+from diets import views
+from rest_framework.urlpatterns import format_suffix_patterns
+from rest_framework_simplejwt.views import (
+    TokenObtainPairView,
+    TokenRefreshView,
+)
+
+# This is a bit messy and will need to change
+urlpatterns = format_suffix_patterns(
+    [
+        path("", views.api_root),
+        path("api/diets/", views.DietList.as_view(), name="diet-list"),
+        path(
+            "api/diets/<int:pk>/",
+            views.DietDetail.as_view(),
+            name="diet-detail",
+        ),
+        path("api/mealtypes/", views.MealtypeList.as_view(), name="mealtype-list"),
+        path(
+            "api/mealtypes/<int:pk>/",
+            views.MealtypeDetail.as_view(),
+            name="mealtype-detail",
+        ),
+        path(
+            "api/mealtype-instances/",
+            views.MealtypeInstanceList.as_view(),
+            name="mealtype-instance-list",
+        ),
+        path(
+            "api/mealtype-instances/<int:pk>/",
+            views.MealtypeInstanceDetail.as_view(),
+            name="mealtypeinstance-detail",
+        ),
+        path(
+            "api/diet-files/",
+            views.DietFileList.as_view(),
+            name="diet-file-list",
+        ),
+        path(
+            "api/diet-files/<int:pk>/",
+            views.DietFileDetail.as_view(),
+            name="dietfile-detail",
+        ),
+        path("", include("users.urls")),
+        path("", include("comments.urls")),
+        path("api/auth/", include("rest_framework.urls")),
+        path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
+        path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
+        path("api/remember_me/", views.RememberMe.as_view(), name="remember_me"),
+    ]
+)
diff --git a/backend/secfit/diets/views.py b/backend/secfit/diets/views.py
new file mode 100644
index 0000000..676b42b
--- /dev/null
+++ b/backend/secfit/diets/views.py
@@ -0,0 +1,342 @@
+"""Contains views for the diets application. These are mostly class-based views.
+"""
+from rest_framework import generics, mixins
+from rest_framework import permissions
+
+from rest_framework.parsers import (
+    JSONParser,
+)
+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 rest_framework import filters
+from diets.parsers import MultipartJsonParser
+from diets.permissions import (
+    IsOwner,
+    IsCoachAndVisibleToCoach,
+    IsOwnerOfDiet,
+    IsCoachOfDietAndVisibleToCoach,
+    IsReadOnly,
+    IsPublic,
+    IsDietPublic,
+)
+from diets.mixins import CreateListModelMixin
+from diets.models import Diet, Mealtype, MealtypeInstance, DietFile
+from diets.serializers import DietSerializer, MealtypeSerializer
+from diets.serializers import RememberMeSerializer
+from diets.serializers import MealtypeInstanceSerializer, DietFileSerializer
+from django.core.exceptions import PermissionDenied
+from rest_framework_simplejwt.tokens import RefreshToken
+from rest_framework.response import Response
+import json
+from collections import namedtuple
+import base64, pickle
+from django.core.signing import Signer
+
+
+@api_view(["GET"])
+def api_root(request, format=None):
+    return Response(
+        {
+            "users": reverse("user-list", request=request, format=format),
+            "diets": reverse("diet-list", request=request, format=format),
+            "mealtypes": reverse("mealtype-list", request=request, format=format),
+            "mealtype-instances": reverse(
+                "mealtype-instance-list", request=request, format=format
+            ),
+            "diet-files": reverse(
+                "diet-file-list", request=request, format=format
+            ),
+            "comments": reverse("comment-list", request=request, format=format),
+            "likes": reverse("like-list", request=request, format=format),
+        }
+    )
+
+
+# Allow users to save a persistent session in their browser
+class RememberMe(
+    mixins.ListModelMixin,
+    mixins.CreateModelMixin,
+    mixins.DestroyModelMixin,
+    generics.GenericAPIView,
+):
+
+    serializer_class = RememberMeSerializer
+
+    def get(self, request):
+        if request.user.is_authenticated == False:
+            raise PermissionDenied
+        else:
+            return Response({"remember_me": self.rememberme()})
+
+    def post(self, request):
+        cookieObject = namedtuple("Cookies", request.COOKIES.keys())(
+            *request.COOKIES.values()
+        )
+        user = self.get_user(cookieObject)
+        refresh = RefreshToken.for_user(user)
+        return Response(
+            {
+                "refresh": str(refresh),
+                "access": str(refresh.access_token),
+            }
+        )
+
+    def get_user(self, cookieObject):
+        decode = base64.b64decode(cookieObject.remember_me)
+        user, sign = pickle.loads(decode)
+
+        # Validate signature
+        if sign == self.sign_user(user):
+            return user
+
+    def rememberme(self):
+        creds = [self.request.user, self.sign_user(str(self.request.user))]
+        return base64.b64encode(pickle.dumps(creds))
+
+    def sign_user(self, username):
+        signer = Signer()
+        signed_user = signer.sign(username)
+        return signed_user
+
+
+class DietList(
+    mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView
+):
+    """Class defining the web response for the creation of a Diet, or displaying a list
+    of Diets
+
+    HTTP methods: GET, POST
+    """
+
+    serializer_class = DietSerializer
+    permission_classes = [
+        permissions.IsAuthenticated
+    ]  # User must be authenticated to create/view diets
+    parser_classes = [
+        MultipartJsonParser,
+        JSONParser,
+    ]  # For parsing JSON and Multi-part requests
+    filter_backends = [filters.OrderingFilter]
+    ordering_fields = ["name", "date", "owner__username"]
+
+    def get(self, request, *args, **kwargs):
+        return self.list(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        return self.create(request, *args, **kwargs)
+
+    def perform_create(self, serializer):
+        serializer.save(owner=self.request.user)
+
+    def get_queryset(self):
+        qs = Diet.objects.none()
+        if self.request.user:
+            # A diet should be visible to the requesting user if any of the following hold:
+            # - The diet has public visibility
+            # - The owner of the diet is the requesting user
+            # - The diet has coach visibility and the requesting user is the owner's coach
+            qs = Diet.objects.filter(
+                Q(visibility="PU")
+                | (Q(visibility="CO") & Q(owner__coach=self.request.user))
+            ).distinct()
+
+        return qs
+
+
+class DietDetail(
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    generics.GenericAPIView,
+):
+    """Class defining the web response for the details of an individual Diet.
+
+    HTTP methods: GET, PUT, DELETE
+    """
+
+    queryset = Diet.objects.all()
+    serializer_class = DietSerializer
+    permission_classes = [
+        permissions.IsAuthenticated
+        & (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic)))
+    ]
+    parser_classes = [MultipartJsonParser, JSONParser]
+
+    def get(self, request, *args, **kwargs):
+        return self.retrieve(request, *args, **kwargs)
+
+    def put(self, request, *args, **kwargs):
+        return self.update(request, *args, **kwargs)
+
+    def delete(self, request, *args, **kwargs):
+        return self.destroy(request, *args, **kwargs)
+
+
+class MealtypeList(
+    mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView
+):
+    """Class defining the web response for the creation of an Mealtype, or
+    a list of Mealtypes.
+
+    HTTP methods: GET, POST
+    """
+
+    queryset = Mealtype.objects.all()
+    serializer_class = MealtypeSerializer
+    permission_classes = [permissions.IsAuthenticated]
+
+    def get(self, request, *args, **kwargs):
+        return self.list(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        return self.create(request, *args, **kwargs)
+
+
+class MealtypeDetail(
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    generics.GenericAPIView,
+):
+    """Class defining the web response for the details of an individual Mealtype.
+
+    HTTP methods: GET, PUT, PATCH, DELETE
+    """
+
+    queryset = Mealtype.objects.all()
+    serializer_class = MealtypeSerializer
+    permission_classes = [permissions.IsAuthenticated]
+
+    def get(self, request, *args, **kwargs):
+        return self.retrieve(request, *args, **kwargs)
+
+    def put(self, request, *args, **kwargs):
+        return self.update(request, *args, **kwargs)
+
+    def patch(self, request, *args, **kwargs):
+        return self.partial_update(request, *args, **kwargs)
+
+    def delete(self, request, *args, **kwargs):
+        return self.destroy(request, *args, **kwargs)
+
+
+class MealtypeInstanceList(
+    mixins.ListModelMixin,
+    mixins.CreateModelMixin,
+    CreateListModelMixin,
+    generics.GenericAPIView,
+):
+    """Class defining the web response for the creation"""
+
+    serializer_class = MealtypeInstanceSerializer
+    permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet]
+
+    def get(self, request, *args, **kwargs):
+        return self.list(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        return self.create(request, *args, **kwargs)
+
+    def get_queryset(self):
+        qs = MealtypeInstance.objects.none()
+        if self.request.user:
+            qs = MealtypeInstance.objects.filter(
+                Q(diet__owner=self.request.user)
+                | (
+                    (Q(diet__visibility="CO") | Q(diet__visibility="PU"))
+                    & Q(diet__owner__coach=self.request.user)
+                )
+            ).distinct()
+
+        return qs
+
+
+class MealtypeInstanceDetail(
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    generics.GenericAPIView,
+):
+    serializer_class = MealtypeInstanceSerializer
+    permission_classes = [
+        permissions.IsAuthenticated
+        & (
+            IsOwnerOfDiet
+            | (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic))
+        )
+    ]
+
+    def get(self, request, *args, **kwargs):
+        return self.retrieve(request, *args, **kwargs)
+
+    def put(self, request, *args, **kwargs):
+        return self.update(request, *args, **kwargs)
+
+    def patch(self, request, *args, **kwargs):
+        return self.partial_update(request, *args, **kwargs)
+
+    def delete(self, request, *args, **kwargs):
+        return self.destroy(request, *args, **kwargs)
+
+
+class DietFileList(
+    mixins.ListModelMixin,
+    mixins.CreateModelMixin,
+    CreateListModelMixin,
+    generics.GenericAPIView,
+):
+
+    queryset = DietFile.objects.all()
+    serializer_class = DietFileSerializer
+    permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet]
+    parser_classes = [MultipartJsonParser, JSONParser]
+
+    def get(self, request, *args, **kwargs):
+        return self.list(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        return self.create(request, *args, **kwargs)
+
+    def perform_create(self, serializer):
+        serializer.save(owner=self.request.user)
+
+    def get_queryset(self):
+        qs = DietFile.objects.none()
+        if self.request.user:
+            qs = DietFile.objects.filter(
+                Q(owner=self.request.user)
+                | Q(diet__owner=self.request.user)
+                | (
+                    Q(diet__visibility="CO")
+                    & Q(diet__owner__coach=self.request.user)
+                )
+            ).distinct()
+
+        return qs
+
+
+class DietFileDetail(
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    generics.GenericAPIView,
+):
+
+    queryset = DietFile.objects.all()
+    serializer_class = DietFileSerializer
+    permission_classes = [
+        permissions.IsAuthenticated
+        & (
+            IsOwner
+            | IsOwnerOfDiet
+            | (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic))
+        )
+    ]
+
+    def get(self, request, *args, **kwargs):
+        return self.retrieve(request, *args, **kwargs)
+
+    def delete(self, request, *args, **kwargs):
+        return self.destroy(request, *args, **kwargs)
diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py
index 3c9ec93..2f71885 100644
--- a/backend/secfit/secfit/settings.py
+++ b/backend/secfit/secfit/settings.py
@@ -58,6 +58,7 @@ INSTALLED_APPS = [
     "django.contrib.staticfiles",
     "rest_framework",
     "workouts.apps.WorkoutsConfig",
+    "diets.apps.DietsConfig",
     "users.apps.UsersConfig",
     "comments.apps.CommentsConfig",
     "corsheaders",
diff --git a/backend/secfit/secfit/urls.py b/backend/secfit/secfit/urls.py
index 3146886..5c5ff90 100644
--- a/backend/secfit/secfit/urls.py
+++ b/backend/secfit/secfit/urls.py
@@ -21,6 +21,7 @@ from django.conf.urls.static import static
 urlpatterns = [
     path("admin/", admin.site.urls),
     path("", include("workouts.urls")),
+    path("", include("diets.urls")),
 ]
 
 urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
diff --git a/frontend/www/diet.html b/frontend/www/diet.html
new file mode 100644
index 0000000..baecd9e
--- /dev/null
+++ b/frontend/www/diet.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Diet</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 Diet</h3>
+            </div>
+        </div>
+    <form class="row g-3 mb-4" id="form-diet">
+        <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-diet" value="  OK  ">
+          <input type="button" class="btn btn-primary hide" id="btn-edit-diet" value=" Edit ">
+          <input type="button" class="btn btn-secondary hide" id="btn-cancel-diet" value="Cancel">
+          <input type="button" class="btn btn-danger float-end hide" id="btn-delete-diet" value="Delete">
+        </div>
+        <div class="col-lg-6"></div>
+        <div class="col-lg-12">
+            <h3 class="mt-3">Mealtypes</h3>
+        </div>
+        <div id="div-mealtypes" class="col-lg-12">
+        </div>
+        <div class="col-lg-6">
+          <input type="button" class="btn btn-primary hide" id="btn-add-mealtype" value="Add mealtype">
+          <input type="button" class="btn btn-danger hide" id="btn-remove-mealtype" value="Remove mealtype">
+        </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-mealtype">
+      <div class="row div-mealtype-container g-3 mb-3">
+        <div class="col-lg-6"><h5>Mealtype</h5></div>
+        <div class="col-lg-6"></div>
+        <div class="col-lg-6">
+          <label class="form-label mealtype-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 mealtype-sets">Sets</label>
+          <input type="number" class="form-control" name="sets">
+        </div>
+        <div class="col-lg-3">
+          <label class="form-label mealtype-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/diet.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/diets.html b/frontend/www/diets.html
new file mode 100644
index 0000000..db92119
--- /dev/null
+++ b/frontend/www/diets.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Diets</title>
+    <link rel="stylesheet" href="styles/style.css">
+    <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>
+    <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 text-center">
+                <h3 class="mt-5">View Diets</h3>
+                <p>Here you can view diets completed by you, your athletes, 
+                    or the public. Click on a diet to view its details.</p>
+                <input type="button" class="btn btn-success" id="btn-create-diet" value="Log new diet">
+            </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-diets-list" data-bs-toggle="list" href="#list-all-diets" role="tab" aria-controls="all">All Diets</a>
+                  <a class="list-group-item list-group-item-action" id="list-my-diets-list" data-bs-toggle="list" href="#list-my-diets" role="tab" aria-controls="my">My Diets</a>
+                  <a class="list-group-item list-group-item-action" id="list-athlete-diets-list" data-bs-toggle="list" href="#list-athlete-diets" role="tab" aria-controls="athlete">Athlete Diets</a>
+                  <a class="list-group-item list-group-item-action" id="list-public-diets-list" data-bs-toggle="list" href="#list-public-diets" role="tab" aria-controls="public">Public Diets</a>
+                </div>
+                <div class="mt-1">Sort by: <a href="?ordering=date">Date</a> <a href="?ordering=owner">Owner</a> <a href="?ordering=name">Name</a>
+                    <br>Currently sorting by: <span id="current-sort"></span>
+                </div>
+                <div class="list-group mt-1" id="div-content"></div>
+              </div>
+        </div>
+
+    </div>
+
+    <template id="template-diet">
+        <a class="list-group-item list-group-item-action flex-column align-items-start my-1 diet">
+            <div class="d-flex w-100 justify-content-between align-items-center">
+                <h5 class="mb-1"></h5>
+            </div>
+            <div class="d-flex">
+                <table class="mb-1 text-start">
+                    <tr><td>Date:</td><td></td></tr>
+                    <tr><td>Time:</td><td></td></tr>
+                    <tr><td>Owner:</td><td></td></tr>
+                    <tr><td>Mealtypes:</td><td></td></tr>
+                </table>       
+            </div>
+        </a>
+    </template>
+
+    <script src="scripts/defaults.js"></script>
+    <script src="scripts/scripts.js"></script>
+    <script src="scripts/diets.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/mealtype.html b/frontend/www/mealtype.html
new file mode 100644
index 0000000..c348379
--- /dev/null
+++ b/frontend/www/mealtype.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Mealtype</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 Mealtype</h3>
+                </div>
+            </div>
+        <form class="row g-3" id="form-mealtype">
+            <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="inputDescription" class="form-label">Description</label>
+                <textarea class="form-control" id="inputDescription" name="description" readonly></textarea>
+            </div>
+            <div class="col-lg-6"></div>
+            <!-- <div class="col-lg-6">
+                <label for="inputUnit" class="form-label">Unit</label>
+                <input type="text" class="form-control" id="inputUnit" name="unit" readonly>
+            </div> -->
+            <div class="col-lg-6"></div>
+            <div class="col-lg-6">
+                <input type="button" class="btn btn-primary hide" id="btn-ok-mealtype" value="  OK  ">
+                <input type="button" class="btn btn-primary" id="btn-edit-mealtype" value=" Edit ">
+                <input type="button" class="btn btn-secondary hide" id="btn-cancel-mealtype" value="Cancel">
+                <input type="button" class="btn btn-danger float-end hide" id="btn-delete-mealtype" value="Delete">
+            </div>
+            <div class="col-lg-6">
+
+            </div>
+        </form>
+        </div>
+        <script src="scripts/defaults.js"></script>
+        <script src="scripts/scripts.js"></script>
+        <script src="scripts/mealtype.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/mealtypes.html b/frontend/www/mealtypes.html
new file mode 100644
index 0000000..65dd8e5
--- /dev/null
+++ b/frontend/www/mealtypes.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Exercises</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 text-center">
+                <h3 class="mt-5">View Mealtypes</h3>
+                <p>Here you can view, create, and edit mealtypes defined by you and other coaches.</p>
+                <input type="button" class="btn btn-primary" id="btn-create-mealtype" value="Create new mealtype">
+        </div>
+        <div class="row">
+            <div class="col-lg text-center">
+                <div class="list-group mt-1" id="div-content"></div>
+            </div>
+        </div>
+    </div>
+
+    <template id="template-mealtype">
+        <a class="list-group-item list-group-item-action flex-column align-items-start my-1 mealtype" href="">
+            <div class="d-flex w-100 justify-content-between align-items-center">
+                <h5 class="mb-1"></h5>
+            </div>
+            <div class="d-flex">
+                <p class="mb-1">
+                </p>
+            </div>
+        </a>
+    </template>
+
+    <script src="scripts/defaults.js"></script>
+    <script src="scripts/scripts.js"></script>
+    <script src="scripts/mealtypes.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/diet.js b/frontend/www/scripts/diet.js
new file mode 100644
index 0000000..52eae73
--- /dev/null
+++ b/frontend/www/scripts/diet.js
@@ -0,0 +1,353 @@
+let cancelDietButton;
+let okDietButton;
+let deleteDietButton;
+let editDietButton;
+let postCommentButton;
+
+async function retrieveDiet(id) {  
+    let dietData = null;
+    let response = await sendRequest("GET", `${HOST}/api/diets/${id}/`);
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert("Could not retrieve diet data!", data);
+        document.body.prepend(alert);
+    } else {
+        dietData = await response.json();
+        let form = document.querySelector("#form-diet");
+        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 = dietData[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 = dietData["visibility"];
+        // files
+        let filesDiv = document.querySelector("#uploaded-files");
+        for (let file of dietData.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 mealtypes
+
+        // fetch mealtype types
+        let mealtypeTypeResponse = await sendRequest("GET", `${HOST}/api/mealtypes/`);
+        let mealtypeTypes = await mealtypeTypeResponse.json();
+
+        //TODO: This should be in its own method.
+        for (let i = 0; i < dietData.mealtype_instances.length; i++) {
+            let templateMealtype = document.querySelector("#template-mealtype");
+            let divMealtypeContainer = templateMealtype.content.firstElementChild.cloneNode(true);
+
+            let mealtypeTypeLabel = divMealtypeContainer.querySelector('.mealtype-type');
+            mealtypeTypeLabel.for = `inputMealtypeType${i}`;
+    
+            let mealtypeTypeSelect = divMealtypeContainer.querySelector("select");            
+            mealtypeTypeSelect.id = `inputMealtypeType${i}`;
+            mealtypeTypeSelect.disabled = true;
+            
+            let splitUrl = dietData.mealtype_instances[i].mealtype.split("/");
+            let currentMealtypeTypeId = splitUrl[splitUrl.length - 2];
+            let currentMealtypeType = "";
+
+            for (let j = 0; j < mealtypeTypes.count; j++) {
+                let option = document.createElement("option");
+                option.value = mealtypeTypes.results[j].id;
+                if (currentMealtypeTypeId == mealtypeTypes.results[j].id) {
+                    currentMealtypeType = mealtypeTypes.results[j];
+                }
+                option.innerText = mealtypeTypes.results[j].name;
+                mealtypeTypeSelect.append(option);
+            }
+            
+            mealtypeTypeSelect.value = currentMealtypeType.id;
+
+            let mealtypeSetLabel = divMealtypeContainer.querySelector('.mealtype-sets');
+            mealtypeSetLabel.for = `inputSets${i}`;
+
+            let mealtypeSetInput = divMealtypeContainer.querySelector("input[name='sets']");
+            mealtypeSetInput.id = `inputSets${i}`;
+            mealtypeSetInput.value = dietData.mealtype_instances[i].sets;
+            mealtypeSetInput.readOnly = true;
+
+            let mealtypeNumberLabel = divMealtypeContainer.querySelector('.mealtype-number');
+            mealtypeNumberLabel.for = "for", `inputNumber${i}`;
+            mealtypeNumberLabel.innerText = currentMealtypeType.unit;
+
+            let mealtypeNumberInput = divMealtypeContainer.querySelector("input[name='number']");
+            mealtypeNumberInput.id = `inputNumber${i}`;
+            mealtypeNumberInput.value = dietData.mealtype_instances[i].number;
+            mealtypeNumberInput.readOnly = true;
+
+            let mealtypesDiv = document.querySelector("#div-mealtypes");
+            mealtypesDiv.appendChild(divMealtypeContainer);
+        }
+    }
+    return dietData;     
+}
+
+function handleCancelDuringDietEdit() {
+    location.reload();
+}
+
+function handleEditDietButtonClick() {
+    let addMealtypeButton = document.querySelector("#btn-add-mealtype");
+    let removeMealtypeButton = document.querySelector("#btn-remove-mealtype");
+    
+    setReadOnly(false, "#form-diet");
+    document.querySelector("#inputOwner").readOnly = true;  // owner field should still be readonly 
+
+    editDietButton.className += " hide";
+    okDietButton.className = okDietButton.className.replace(" hide", "");
+    cancelDietButton.className = cancelDietButton.className.replace(" hide", "");
+    deleteDietButton.className = deleteDietButton.className.replace(" hide", "");
+    addMealtypeButton.className = addMealtypeButton.className.replace(" hide", "");
+    removeMealtypeButton.className = removeMealtypeButton.className.replace(" hide", "");
+
+    cancelDietButton.addEventListener("click", handleCancelDuringDietEdit);
+
+}
+
+async function deleteDiet(id) {
+    let response = await sendRequest("DELETE", `${HOST}/api/diets/${id}/`);
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert(`Could not delete diet ${id}!`, data);
+        document.body.prepend(alert);
+    } else {
+        window.location.replace("diets.html");
+    }
+}
+
+async function updateDiet(id) {
+    let submitForm = generateDietForm();
+
+    let response = await sendRequest("PUT", `${HOST}/api/diets/${id}/`, submitForm, "");
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert("Could not update diet!", data);
+        document.body.prepend(alert);
+    } else {
+        location.reload();
+    }
+}
+
+function generateDietForm() {
+    let form = document.querySelector("#form-diet");
+
+    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 mealtype instances
+    let mealtypeInstances = [];
+    let mealtypeInstancesTypes = formData.getAll("type");
+    let mealtypeInstancesSets = formData.getAll("sets");
+    let mealtypeInstancesNumbers = formData.getAll("number");
+    for (let i = 0; i < mealtypeInstancesTypes.length; i++) {
+        mealtypeInstances.push({
+            mealtype: `${HOST}/api/mealtypes/${mealtypeInstancesTypes[i]}/`,
+            number: mealtypeInstancesNumbers[i],
+            sets: mealtypeInstancesSets[i]
+        });
+    }
+
+    submitForm.append("mealtype_instances", JSON.stringify(mealtypeInstances));
+    // adding files
+    for (let file of formData.getAll("files")) {
+        submitForm.append("files", file);
+    }
+    return submitForm;
+}
+
+async function createDiet() {
+    let submitForm = generateDietForm();
+
+    let response = await sendRequest("POST", `${HOST}/api/diets/`, submitForm, "");
+
+    if (response.ok) {
+        window.location.replace("diets.html");
+    } else {
+        let data = await response.json();
+        let alert = createAlert("Could not create new diet!", data);
+        document.body.prepend(alert);
+    }
+}
+
+function handleCancelDuringDietCreate() {
+    window.location.replace("diets.html");
+}
+
+async function createBlankMealtype() {
+    let form = document.querySelector("#form-diet");
+
+    let mealtypeTypeResponse = await sendRequest("GET", `${HOST}/api/mealtypes/`);
+    let mealtypeTypes = await mealtypeTypeResponse.json();
+
+    let mealtypeTemplate = document.querySelector("#template-mealtype");
+    let divMealtypeContainer = mealtypeTemplate.content.firstElementChild.cloneNode(true);
+    let mealtypeTypeSelect = divMealtypeContainer.querySelector("select");
+    
+    for (let i = 0; i < mealtypeTypes.count; i++) {
+        let option = document.createElement("option");
+        option.value = mealtypeTypes.results[i].id;
+        option.innerText = mealtypeTypes.results[i].name;
+        mealtypeTypeSelect.append(option);
+    }
+
+    let currentMealtypeType = mealtypeTypes.results[0];
+    mealtypeTypeSelect.value = currentMealtypeType.name;
+    
+    let divMealtypes = document.querySelector("#div-mealtypes");
+    divMealtypes.appendChild(divMealtypeContainer);
+}
+
+function removeMealtype(event) {
+    let divMealtypeContainers = document.querySelectorAll(".div-mealtype-container");
+    if (divMealtypeContainers && divMealtypeContainers.length > 0) {
+        divMealtypeContainers[divMealtypeContainers.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(dietid) {
+    let commentArea = document.querySelector("#comment-area");
+    let content = commentArea.value;
+    let body = {diet: `${HOST}/api/diets/${dietid}/`, 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(dietid) {
+    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.diet.split("/");
+            if (splitArray[splitArray.length - 2] == dietid) {
+                addComment(comment.owner, comment.content, comment.timestamp, true);
+            }
+        }
+    }
+}
+
+window.addEventListener("DOMContentLoaded", async () => {
+    cancelDietButton = document.querySelector("#btn-cancel-diet");
+    okDietButton = document.querySelector("#btn-ok-diet");
+    deleteDietButton = document.querySelector("#btn-delete-diet");
+    editDietButton = document.querySelector("#btn-edit-diet");
+    let postCommentButton = document.querySelector("#post-comment");
+    let divCommentRow = document.querySelector("#div-comment-row");
+    let buttonAddMealtype = document.querySelector("#btn-add-mealtype");
+    let buttonRemoveMealtype = document.querySelector("#btn-remove-mealtype");
+
+    buttonAddMealtype.addEventListener("click", createBlankMealtype);
+    buttonRemoveMealtype.addEventListener("click", removeMealtype);
+
+    const urlParams = new URLSearchParams(window.location.search);
+    let currentUser = await getCurrentUser();
+
+    if (urlParams.has('id')) {
+        const id = urlParams.get('id');
+        let dietData = await retrieveDiet(id);
+        await retrieveComments(id);
+
+        if (dietData["owner"] == currentUser.url) {
+            editDietButton.classList.remove("hide");
+            editDietButton.addEventListener("click", handleEditDietButtonClick);
+            deleteDietButton.addEventListener("click", (async (id) => await deleteDiet(id)).bind(undefined, id));
+            okDietButton.addEventListener("click", (async (id) => await updateDiet(id)).bind(undefined, id));
+            postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id));
+            divCommentRow.className = divCommentRow.className.replace(" hide", "");
+        }
+    } else {
+        await createBlankMealtype();
+        let ownerInput = document.querySelector("#inputOwner");
+        ownerInput.value = currentUser.username;
+        setReadOnly(false, "#form-diet");
+        ownerInput.readOnly = !ownerInput.readOnly;
+
+        okDietButton.className = okDietButton.className.replace(" hide", "");
+        cancelDietButton.className = cancelDietButton.className.replace(" hide", "");
+        buttonAddMealtype.className = buttonAddMealtype.className.replace(" hide", "");
+        buttonRemoveMealtype.className = buttonRemoveMealtype.className.replace(" hide", "");
+
+        okDietButton.addEventListener("click", async () => await createDiet());
+        cancelDietButton.addEventListener("click", handleCancelDuringDietCreate);
+        divCommentRow.className += " hide";
+    }
+
+});
\ No newline at end of file
diff --git a/frontend/www/scripts/diets.js b/frontend/www/scripts/diets.js
new file mode 100644
index 0000000..0ee8125
--- /dev/null
+++ b/frontend/www/scripts/diets.js
@@ -0,0 +1,106 @@
+async function fetchDiets(ordering) {
+    let response = await sendRequest("GET", `${HOST}/api/diets/?ordering=${ordering}`);
+
+    if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+    } else {
+        let data = await response.json();
+
+        let diets = data.results;
+        let container = document.getElementById('div-content');
+        diets.forEach(diet => {
+            let templateDiet = document.querySelector("#template-diet");
+            let cloneDiet = templateDiet.content.cloneNode(true);
+
+            let aDiet = cloneDiet.querySelector("a");
+            aDiet.href = `diet.html?id=${diet.id}`;
+
+            let h5 = aDiet.querySelector("h5");
+            h5.textContent = diet.name;
+
+            let localDate = new Date(diet.date);
+
+            let table = aDiet.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 = diet.owner_username; //Owner
+            rows[3].querySelectorAll("td")[1].textContent = diet.mealtype_instances.length; // Mealtypes
+
+            container.appendChild(aDiet);
+        });
+        return diets;
+    }
+}
+
+function createDiet() {
+    window.location.replace("diet.html");
+}
+
+window.addEventListener("DOMContentLoaded", async () => {
+    let createButton = document.querySelector("#btn-create-diet");
+    createButton.addEventListener("click", createDiet);
+    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 diets = await fetchDiets(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 dietAnchors = document.querySelectorAll('.diet');
+            for (let j = 0; j < diets.length; j++) {
+                // I'm assuming that the order of diet objects matches
+                // the other of the diet anchor elements. They should, given
+                // that I just created them.
+                let diet = diets[j];
+                let dietAnchor = dietAnchors[j];
+
+                switch (event.currentTarget.id) {
+                    case "list-my-diets-list":
+                        if (diet.owner == currentUser.url) {
+                            dietAnchor.classList.remove('hide');
+                        } else {
+                            dietAnchor.classList.add('hide');
+                        }
+                        break;
+                    case "list-athlete-diets-list":
+                        if (currentUser.athletes && currentUser.athletes.includes(diet.owner)) {
+                            dietAnchor.classList.remove('hide');
+                        } else {
+                            dietAnchor.classList.add('hide');
+                        }
+                        break;
+                    case "list-public-diets-list":
+                        if (diet.visibility == "PU") {
+                            dietAnchor.classList.remove('hide');
+                        } else {
+                            dietAnchor.classList.add('hide');
+                        }
+                        break;
+                    default :
+                        dietAnchor.classList.remove('hide');
+                        break;
+                }
+            }
+        });
+    }
+});
\ No newline at end of file
diff --git a/frontend/www/scripts/mealtype.js b/frontend/www/scripts/mealtype.js
new file mode 100644
index 0000000..16a3c4a
--- /dev/null
+++ b/frontend/www/scripts/mealtype.js
@@ -0,0 +1,156 @@
+let cancelButton;
+let okButton;
+let deleteButton;
+let editButton;
+let oldFormData;
+
+
+function handleCancelButtonDuringEdit() {
+    setReadOnly(true, "#form-mealtype");
+    okButton.className += " hide";
+    deleteButton.className += " hide";
+    cancelButton.className += " hide";
+    editButton.className = editButton.className.replace(" hide", "");
+
+    cancelButton.removeEventListener("click", handleCancelButtonDuringEdit);
+
+    let form = document.querySelector("#form-mealtype");
+    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("mealtypes.html");
+}
+
+async function createMealtype() {
+    let form = document.querySelector("#form-mealtype");
+    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/mealtypes/`, body);
+
+    if (response.ok) {
+        window.location.replace("mealtypes.html");
+    } else {
+        let data = await response.json();
+        let alert = createAlert("Could not create new mealtype!", data);
+        document.body.prepend(alert);
+    }
+}
+
+function handleEditMealtypeButtonClick() {
+    setReadOnly(false, "#form-mealtype");
+
+    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);
+
+    let form = document.querySelector("#form-mealtype");
+    oldFormData = new FormData(form);
+}
+
+async function deleteMealtype(id) {
+    let response = await sendRequest("DELETE", `${HOST}/api/mealtypes/${id}/`);
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert(`Could not delete mealtype ${id}`, data);
+        document.body.prepend(alert);
+    } else {
+        window.location.replace("mealtypes.html");
+    }
+}
+
+async function retrieveMealtype(id) {
+    let response = await sendRequest("GET", `${HOST}/api/mealtypes/${id}/`);
+    console.log(response.ok);
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert("Could not retrieve mealtype data!", data);
+        document.body.prepend(alert);
+    } else {
+        let mealtypeData = await response.json();
+        let form = document.querySelector("#form-mealtype");
+        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 = mealtypeData[key];
+            input.value = newVal;
+        }
+    }
+}
+
+async function updateMealtype(id) {
+    let form = document.querySelector("#form-mealtype");
+    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/mealtypes/${id}/`, body);
+
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert(`Could not update mealtype ${id}`, data);
+        document.body.prepend(alert);
+    } else {
+        // duplicate code from handleCancelButtonDuringEdit
+        // you should refactor this
+        setReadOnly(true, "#form-mealtype");
+        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");
+    }
+}
+
+window.addEventListener("DOMContentLoaded", async () => {
+    cancelButton = document.querySelector("#btn-cancel-mealtype");
+    okButton = document.querySelector("#btn-ok-mealtype");
+    deleteButton = document.querySelector("#btn-delete-mealtype");
+    editButton = document.querySelector("#btn-edit-mealtype");
+    oldFormData = null;
+
+    const urlParams = new URLSearchParams(window.location.search);
+
+    // view/edit
+    if (urlParams.has('id')) {
+        const mealtypeId = urlParams.get('id');
+        await retrieveMealtype(mealtypeId);
+
+        editButton.addEventListener("click", handleEditMealtypeButtonClick);
+        deleteButton.addEventListener("click", (async (id) => await deleteMealtype(id)).bind(undefined, mealtypeId));
+        okButton.addEventListener("click", (async (id) => await updateMealtype(id)).bind(undefined, mealtypeId));
+    } 
+    //create
+    else {
+        setReadOnly(false, "#form-mealtype");
+
+        editButton.className += " hide";
+        okButton.className = okButton.className.replace(" hide", "");
+        cancelButton.className = cancelButton.className.replace(" hide", "");
+
+        okButton.addEventListener("click", async () => await createMealtype());
+        cancelButton.addEventListener("click", handleCancelButtonDuringCreate);
+    }
+});
\ No newline at end of file
diff --git a/frontend/www/scripts/mealtypes.js b/frontend/www/scripts/mealtypes.js
new file mode 100644
index 0000000..5096c71
--- /dev/null
+++ b/frontend/www/scripts/mealtypes.js
@@ -0,0 +1,42 @@
+async function fetchMealtypes(request) {
+    let response = await sendRequest("GET", `${HOST}/api/mealtypes/`);
+
+    if (response.ok) {
+        let data = await response.json();
+
+        let mealtypes = data.results;
+        let container = document.getElementById('div-content');
+        let mealtypeTemplate = document.querySelector("#template-mealtype");
+        mealtypes.forEach(mealtype => {
+            const mealtypeAnchor = mealtypeTemplate.content.firstElementChild.cloneNode(true);
+            mealtypeAnchor.href = `mealtype.html?id=${mealtype.id}`;
+
+            const h5 = mealtypeAnchor.querySelector("h5");
+            h5.textContent = mealtype.name;
+
+            const p = mealtypeAnchor.querySelector("p");
+            p.textContent = mealtype.description;   
+
+            container.appendChild(mealtypeAnchor);
+        });
+    }
+
+    return response;
+}
+
+function createMealtype() {
+    window.location.replace("mealtype.html");
+}
+
+window.addEventListener("DOMContentLoaded", async () => {
+    let createButton = document.querySelector("#btn-create-mealtype");
+    createButton.addEventListener("click", createMealtype);
+
+    let response = await fetchMealtypes();
+    
+    if (!response.ok) {
+        let data = await response.json();
+        let alert = createAlert("Could not retrieve mealtypes!", data);
+        document.body.prepend(alert);
+    }
+});
\ No newline at end of file
diff --git a/frontend/www/scripts/navbar.js b/frontend/www/scripts/navbar.js
index d906f61..9b3c29c 100644
--- a/frontend/www/scripts/navbar.js
+++ b/frontend/www/scripts/navbar.js
@@ -18,6 +18,8 @@ class NavBar extends HTMLElement {
                 <a class="nav-link hide" id="nav-exercises" href="exercises.html">Exercises</a>
                 <a class="nav-link hide" id="nav-mycoach" href="mycoach.html">Coach</a>
                 <a class="nav-link hide" id="nav-myathletes" href="myathletes.html">Athletes</a>
+                <a class="nav-link hide" id="nav-mealtypes" href="mealtypes.html">Mealtypes</a>
+                <a class="nav-link hide" id="nav-diets" href="diets.html">Diets</a>
                 <hr>
             </div>
             <div class="my-2 my-lg-0 me-5">
diff --git a/frontend/www/scripts/scripts.js b/frontend/www/scripts/scripts.js
index 8c55000..b4ea272 100644
--- a/frontend/www/scripts/scripts.js
+++ b/frontend/www/scripts/scripts.js
@@ -22,6 +22,10 @@ function updateNavBar() {
     makeNavLinkActive("nav-mycoach")
   } else if (window.location.pathname == "/myathletes.html") {
     makeNavLinkActive("nav-myathletes");
+  } else if (window.location.pathname == "/mealtypes.html") {
+    makeNavLinkActive("nav-mealtypes");
+  } else if (window.location.pathname == "/diets.html") {
+    makeNavLinkActive("nav-diets");
   }
 
   if (isUserAuthenticated()) {
@@ -32,6 +36,8 @@ function updateNavBar() {
     document.querySelector('a[href="mycoach.html"').classList.remove("hide");
     document.querySelector('a[href="exercises.html"').classList.remove("hide");
     document.querySelector('a[href="myathletes.html"').classList.remove("hide");
+    document.querySelector('a[href="mealtypes.html"').classList.remove("hide");
+    document.querySelector('a[href="diets.html"').classList.remove("hide");
   } else {
     document.getElementById("btn-login-nav").classList.remove("hide");
     document.getElementById("btn-register").classList.remove("hide");
-- 
GitLab