diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..10d45d48 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Backend test CI + +on: + - pull_request + +jobs: + backend-test: + runs-on: self-hosted + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + working-directory: ./ + run: | + python -m pip install --upgrade pip + pip install -r ./backend/requirements.txt + - name: Running Django tests + run: | + sh ./backend/runtests.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ed07993..e4642805 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env django/db.sqlite3 backend/db.sqlite3 +backend/uploads venv/ .venv/ diff --git a/Makefile b/Makefile index 01c1abef..e2a56ff0 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,17 @@ stop: lint: docker exec pigeonhole-backend flake8 . - docker exec pigeonhole-frontend npm run lint \ No newline at end of file + docker exec pigeonhole-frontend npm run lint + +superuser: + docker exec -it pigeonhole-backend python manage.py createsuperuser + +reset: + docker image prune -af + docker system prune + +backendtest: + docker exec -it pigeonhole-backend sh /usr/src/app/backend/runtests.sh + +backendshell: + docker exec -it pigeonhole-backend sh diff --git a/backend/pigeonhole/apps/courses/migrations/0001_initial.py b/backend/pigeonhole/apps/courses/migrations/0001_initial.py index 9d548232..2df87f49 100644 --- a/backend/pigeonhole/apps/courses/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/courses/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py new file mode 100644 index 00000000..f7939197 --- /dev/null +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -0,0 +1,23 @@ +from rest_framework import permissions + +from backend.pigeonhole.apps.users.models import User + + +class CourseUserPermissions(permissions.BasePermission): + def has_permission(self, request, view): + if request.user.is_admin or request.user.is_superuser: + return True + + if request.user.is_teacher: + if view.action in ['create', 'list', 'retrieve']: + return True + elif view.action in ['update', 'partial_update', 'destroy'] and User.objects.filter(id=request.user.id, + course=view.kwargs[ + 'pk']).exists(): + return True + return + + if request.user.is_student or request.user.is_teacher: + return view.action in ['list', 'retrieve'] + + return False diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py new file mode 100644 index 00000000..ff565f65 --- /dev/null +++ b/backend/pigeonhole/apps/courses/views.py @@ -0,0 +1,74 @@ +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from backend.pigeonhole.apps.users.models import User +from .models import Course, CourseSerializer +from .permissions import CourseUserPermissions + + +class CourseViewSet(viewsets.ModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + permission_classes = [IsAuthenticated, CourseUserPermissions] + + def create(self, request, *args, **kwargs): + serializer = CourseSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + course.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + def list(self, request, *args, **kwargs): + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def my_list(self, request, *args, **kwargs): + if request.user.is_admin or request.user.is_superuser: + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + if request.user.is_teacher or request.user.is_student: + courses = User.objects.get(id=request.user.id).course.all() + serializer = CourseSerializer(courses, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, many=False) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['post']) + def join_course(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + user = User.objects.get(id=request.user.id) + user.course.add(course) + return Response(status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/apps/groups/migrations/0001_initial.py b/backend/pigeonhole/apps/groups/migrations/0001_initial.py index fc8925aa..b2685687 100644 --- a/backend/pigeonhole/apps/groups/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/groups/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -16,9 +17,9 @@ class Migration(migrations.Migration): name='Group', fields=[ ('group_id', models.BigAutoField(primary_key=True, serialize=False)), - ('group_nr', models.IntegerField()), + ('group_nr', models.IntegerField(blank=True, null=True)), ('feedback', models.TextField(null=True)), - ('final_score', models.IntegerField()), + ('final_score', models.IntegerField(blank=True, null=True)), ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), diff --git a/backend/pigeonhole/apps/groups/migrations/0002_initial.py b/backend/pigeonhole/apps/groups/migrations/0002_initial.py index 354b0401..61ffe259 100644 --- a/backend/pigeonhole/apps/groups/migrations/0002_initial.py +++ b/backend/pigeonhole/apps/groups/migrations/0002_initial.py @@ -1,20 +1,22 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 +from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ ('groups', '0001_initial'), - ('users', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( model_name='group', - name='student', - field=models.ManyToManyField(to='users.student'), + name='user', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py b/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py deleted file mode 100644 index 51d6180a..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('groups', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='group', - name='group_nr', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py b/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py deleted file mode 100644 index 1a72c57e..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('groups', '0003_alter_group_group_nr'), - ] - - operations = [ - migrations.AlterField( - model_name='group', - name='final_score', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/groups/models.py b/backend/pigeonhole/apps/groups/models.py index 9f57317e..cf42ba04 100644 --- a/backend/pigeonhole/apps/groups/models.py +++ b/backend/pigeonhole/apps/groups/models.py @@ -1,26 +1,39 @@ from django.db import models from rest_framework import serializers +from django.core.exceptions import ValidationError from backend.pigeonhole.apps.projects.models import Project -from backend.pigeonhole.apps.users.models import Student +from backend.pigeonhole.apps.users.models import User class Group(models.Model): group_id = models.BigAutoField(primary_key=True) group_nr = models.IntegerField(blank=True, null=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - student = models.ManyToManyField(Student) + user = models.ManyToManyField(User) feedback = models.TextField(null=True) final_score = models.IntegerField(null=True, blank=True) objects = models.Manager() + # a student can only be in one group per project + def clean(self): + for student in self.student.all(): + existing_groups = Group.objects.filter( + project_id=self.project_id, student=student).exclude( + group_id=self.group_id) + if existing_groups.exists(): + raise ValidationError(f"Student {student} is already part of " + "another group in this project.") + + # a student can only be in one group per project, group_nr is + # automatically assigned and unique per project def save(self, *args, **kwargs): if not self.group_id: if self.group_nr is None: max_group_nr = Group.objects.filter( project_id=self.project_id).aggregate( - models.Max('group_nr'))['group_nr__max'] or 0 + models.Max('group_nr'))['group_nr__max'] or 0 self.group_nr = max_group_nr + 1 super().save(*args, **kwargs) @@ -28,4 +41,4 @@ def save(self, *args, **kwargs): class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group - fields = ["group_id", "group_nr", "final_score", "project_id", "student"] + fields = ["group_id", "group_nr", "final_score", "project_id", "user", "feedback"] diff --git a/backend/pigeonhole/apps/groups/permission.py b/backend/pigeonhole/apps/groups/permission.py new file mode 100644 index 00000000..5672d2c3 --- /dev/null +++ b/backend/pigeonhole/apps/groups/permission.py @@ -0,0 +1,18 @@ +from rest_framework import permissions + + +class CanAccessProject(permissions.BasePermission): + # Custom user class to check if the user can join a group. + def has_permission(self, request, view): + user = request.user + course_id = view.kwargs.get('course_id') + + if user.is_admin or user.is_superuser: + return True + elif user.is_teacher: + if user.course.filter(course_id=course_id).exists(): + return True + elif user.is_student: + if user.course.filter(course_id=course_id).exists(): + return True + return False diff --git a/backend/pigeonhole/apps/groups/views.py b/backend/pigeonhole/apps/groups/views.py new file mode 100644 index 00000000..2ec62913 --- /dev/null +++ b/backend/pigeonhole/apps/groups/views.py @@ -0,0 +1,53 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.groups.models import Group, GroupSerializer +from backend.pigeonhole.apps.projects.models import Project + +# TODO tests for score/max_score + + +class GroupViewSet(viewsets.ModelViewSet): + queryset = Group.objects.all() + serializer_class = GroupSerializer + permission_classes = [IsAuthenticated] + + def create(self, request, *args, **kwargs): + # TODO zorg dat er geen 2 groepen met hetzelfde nummer kunnen zijn. + course_id = kwargs.get('course_id') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + serializer = GroupSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": "You are not allowed to create a new group."}, + status=status.HTTP_400_BAD_REQUEST) + + def list(self, request, *args, **kwargs): + serializer = GroupSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('project_id') + group_id = kwargs.get('pk') + + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + # Check whether the project exists + get_object_or_404(Project, pk=project_id) + group = get_object_or_404(Group, group_id=group_id) + + serializer = GroupSerializer(instance=group, many=False) + + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/apps/projects/migrations/0001_initial.py b/backend/pigeonhole/apps/projects/migrations/0001_initial.py index c4390c94..3101940e 100644 --- a/backend/pigeonhole/apps/projects/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/projects/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -18,6 +19,7 @@ class Migration(migrations.Migration): ('project_id', models.BigAutoField(primary_key=True, serialize=False)), ('name', models.CharField(max_length=256)), ('description', models.TextField()), + ('deadline', models.DateTimeField(blank=True, null=True)), ('visible', models.BooleanField(default=False)), ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.course')), ], @@ -26,7 +28,7 @@ class Migration(migrations.Migration): name='ForbiddenExtension', fields=[ ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.IntegerField()), + ('extension', models.CharField(max_length=512)), ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), @@ -34,19 +36,17 @@ class Migration(migrations.Migration): name='Conditions', fields=[ ('condition_id', models.BigAutoField(primary_key=True, serialize=False)), - ('condition', models.CharField(max_length=256)), - ('deadline', models.DateTimeField()), + ('condition', models.TextField(max_length=256)), ('test_file_location', models.CharField(max_length=512, null=True)), ('test_file_type', models.CharField(max_length=256, null=True)), - ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - to='projects.project')), + ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), migrations.CreateModel( name='AllowedExtension', fields=[ ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.IntegerField()), + ('extension', models.CharField(max_length=512)), ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index 1183fdcc..8834cf12 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -6,24 +6,25 @@ # Create your models here. class Project(models.Model): + objects = models.Manager() project_id = models.BigAutoField(primary_key=True) course_id = models.ForeignKey(Course, on_delete=models.CASCADE) name = models.CharField(max_length=256) description = models.TextField() + deadline = models.DateTimeField(null=True, blank=True) visible = models.BooleanField(default=False) class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ['project_id', 'course_id', 'name', 'description', 'deadline', 'visible'] + fields = ['project_id', 'course_id', 'name', 'description', 'visible', 'deadline'] class Conditions(models.Model): condition_id = models.BigAutoField(primary_key=True) submission_id = models.ForeignKey(Project, on_delete=models.CASCADE) - condition = models.CharField(max_length=256) - deadline = models.DateTimeField() + condition = models.TextField(max_length=256) test_file_location = models.CharField(max_length=512, null=True) test_file_type = models.CharField(max_length=256, null=True) @@ -31,17 +32,17 @@ class Conditions(models.Model): @property def get_forbidden_extensions(self): - return ForbiddenExtension.objects.filter(project_id=self.submission_id) + return ForbiddenExtension.objects.filter(project_id=self.project_id) @property def get_allowed_extensions(self): - return AllowedExtension.objects.filter(project_id=self.submission_id) + return AllowedExtension.objects.filter(project_id=self.project_id) class AllowedExtension(models.Model): extension_id = models.BigAutoField(primary_key=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - extension = models.IntegerField() + extension = models.CharField(max_length=512) objects = models.Manager() @@ -49,6 +50,6 @@ class AllowedExtension(models.Model): class ForbiddenExtension(models.Model): extension_id = models.BigAutoField(primary_key=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - extension = models.IntegerField() + extension = models.CharField(max_length=512) objects = models.Manager() diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py new file mode 100644 index 00000000..c350e5e5 --- /dev/null +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -0,0 +1,18 @@ +from rest_framework import permissions + + +class CanAccessProject(permissions.BasePermission): + # Custom permission class to determine if the currect user has access + # to the project data. + def has_permission(self, request, view): + user = request.user + course_id = view.kwargs.get('course_id') + if user.is_admin or user.is_superuser: + return True + elif user.is_teacher: + if user.course.filter(course_id=course_id).exists(): + return True + elif user.is_student: + if user.course.filter(course_id=course_id).exists(): + return view.action in ['list', 'retrieve'] + return False diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py new file mode 100644 index 00000000..f1f19cf9 --- /dev/null +++ b/backend/pigeonhole/apps/projects/views.py @@ -0,0 +1,103 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from .models import Project, ProjectSerializer, Course +from .permissions import CanAccessProject + +# TODO hier nog zorgen als een project niet visible is, dat de students het niet kunnen zien. +# TODO tests for visibility and deadline + +class ProjectViewSet(viewsets.ModelViewSet): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [IsAuthenticated & CanAccessProject] + + def list(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) + + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + serializer = ProjectSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": "You are not allowed to create a new project."}, + status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + # Check whether the project exists + project = get_object_or_404(Project, pk=project_id) + project.delete() + return Response({"message": "Project has been deleted successfully."}, + status=status.HTTP_204_NO_CONTENT) + return Response({"message": "You are not allowed to delete this project."}, + status=status.HTTP_403_FORBIDDEN) + + def retrieve(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') + + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + # Check whether the project exists + project = get_object_or_404(Project, pk=project_id) + serializer = ProjectSerializer(instance=project, many=False) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + get_object_or_404(Course, course_id=course_id) + + project = get_object_or_404(Project, pk=project_id) + serializer = ProjectSerializer(project, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({"message": "You are not allowed to update this project."}, + status=status.HTTP_403_FORBIDDEN) + + def partial_update(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + # Check whether the project exists + project = get_object_or_404(Project, pk=project_id) + + serializer = ProjectSerializer(project, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({"message": "You are not allowed to update this project."}, + status=status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py index f393bd58..4c3e9dd1 100644 --- a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -17,13 +18,9 @@ class Migration(migrations.Migration): fields=[ ('submission_id', models.BigAutoField(primary_key=True, serialize=False)), ('submission_nr', models.IntegerField()), - ('file', models.FileField( - upload_to='uploads///')), + ('file', models.FileField(max_length=255, null=True, upload_to='uploads/submissions/files///')), ('timestamp', models.DateTimeField(auto_now_add=True)), - ('output_test', models.FileField( - upload_to='uploads///output_test/')), + ('output_test', models.FileField(max_length=255, null=True, upload_to='uploads/submissions/outputs///output_test/')), ('group_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), ], ), diff --git a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index 56752ac0..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('submissions', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField(null=True, - upload_to='uploads///'), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField(null=True, - upload_to='uploads///output_test/'), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index 48eb7b6e..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-05 11:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('submissions', - '0002_alter_submissions_file_alter_submissions_output_test'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField(max_length=255, null=True, - upload_to='uploads///',), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField(max_length=255, null=True, - upload_to='uploads///output_test/',), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index c2aa722a..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('submissions', - '0003_alter_submissions_file_alter_submissions_output_test'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField( - max_length=255, - null=True, - upload_to='uploads/submissions/files///'), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField( - max_length=255, - null=True, - upload_to='uploads/submissions/outputs//' - '/output_test/'), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/models.py b/backend/pigeonhole/apps/submissions/models.py index 7025fb02..7d9f035a 100644 --- a/backend/pigeonhole/apps/submissions/models.py +++ b/backend/pigeonhole/apps/submissions/models.py @@ -1,5 +1,6 @@ from django.db import models from rest_framework import serializers +from django.core.exceptions import ValidationError from backend.pigeonhole.apps.groups.models import Group @@ -10,18 +11,27 @@ class Submissions(models.Model): group_id = models.ForeignKey(Group, on_delete=models.CASCADE, blank=False) submission_nr = models.IntegerField() file = models.FileField(upload_to='uploads/submissions/files/' + - str(group_id) + '/' + str(submission_nr) + '/', + str(group_id) + '/' + str(submission_nr) + '/', null=True, blank=False, max_length=255) timestamp = models.DateTimeField(auto_now_add=True) output_test = models.FileField(upload_to='uploads/submissions/outputs/' + - str(group_id) + '/' + str(submission_nr) + - '/output_test/', null=True, blank=False, + str(group_id) + '/' + str(submission_nr) + + '/output_test/', null=True, blank=False, max_length=255) objects = models.Manager() + # submission_nr is automatically assigned and unique per group, and + # increments + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if not self.submission_id: + max_submission_nr = Submissions.objects.filter( + group_id=self.group_id).aggregate( + models.Max('submission_nr'))['submission_nr__max'] or 0 + self.submission_nr = max_submission_nr + 1 + super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) class SubmissionsSerializer(serializers.ModelSerializer): class Meta: model = Submissions - fields = ['submission_id', 'group_id', 'file', 'timestamp', 'submission_nr'] + fields = ['submission_id', 'group_id', 'file', 'timestamp', 'submission_nr', 'output_test'] diff --git a/backend/pigeonhole/apps/submissions/views.py b/backend/pigeonhole/apps/submissions/views.py new file mode 100644 index 00000000..67a42403 --- /dev/null +++ b/backend/pigeonhole/apps/submissions/views.py @@ -0,0 +1,28 @@ +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from backend.pigeonhole.apps.submissions.models import Submissions, SubmissionsSerializer + +# TODO test timestamp, file, output_test + + +class SubmissionsViewset(viewsets.ModelViewSet): + queryset = Submissions.objects.all() + serializer_class = SubmissionsSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def list(self, request, *args, **kwargs): + serializer = SubmissionsSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request, *args, **kwargs): + serializer = SubmissionsSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/pigeonhole/apps/users/admin.py b/backend/pigeonhole/apps/users/admin.py new file mode 100644 index 00000000..b674d1fc --- /dev/null +++ b/backend/pigeonhole/apps/users/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + +from backend.pigeonhole.apps.users.models import User + + +class UserAdmin(BaseUserAdmin): + list_display = ('email',) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + fieldsets = ( + (None, {'fields': ( + 'email', + 'password', + 'first_name', + 'last_name', + )}), + ) + + +admin.site.register(User, UserAdmin) diff --git a/backend/pigeonhole/apps/users/migrations/0001_initial.py b/backend/pigeonhole/apps/users/migrations/0001_initial.py index 42953dcd..4b63fba7 100644 --- a/backend/pigeonhole/apps/users/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/users/migrations/0001_initial.py @@ -1,14 +1,13 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-12 13:29 import django.contrib.auth.models import django.contrib.auth.validators -import django.db.models.deletion import django.utils.timezone -from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -20,38 +19,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, - help_text='Designates that this user has all permissions ' - 'without explicitly assigning them.', - verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, - help_text='Required. 150 characters or fewer. Letters, digits ' - 'and @/./+/-/_ only.', - max_length=150, unique=True, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name='username')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, - help_text='Designates whether the user can log into this admin site.', - verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, - help_text='Designates whether this user should be treated as active. ' - 'Unselect this instead of deleting accounts.', - verbose_name='active')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, - help_text='The groups this user belongs to. A user will get all ' - 'permissions granted to each of their groups.', - related_name='user_set', related_query_name='user', to='auth.group', - verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', - related_name='user_set', related_query_name='user', - to='auth.permission', verbose_name='user permissions')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('role', models.IntegerField(choices=[(1, 'Admin'), (2, 'Teacher'), (3, 'Student')], default=3)), + ('course', models.ManyToManyField(to='courses.course')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', @@ -63,23 +45,4 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), - migrations.CreateModel( - name='Student', - fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), - ('number', models.IntegerField()), - ('course', models.ManyToManyField(to='courses.course')), - ], - ), - migrations.CreateModel( - name='Teacher', - fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), - ('is_admin', models.BooleanField(default=False)), - ('is_assistent', models.BooleanField(default=False)), - ('course', models.ManyToManyField(to='courses.course')), - ], - ), ] diff --git a/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py b/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py deleted file mode 100644 index a9cf87be..00000000 --- a/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 18:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='teacher', - old_name='is_assistent', - new_name='is_assistant', - ), - ] diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index 9a0f36f4..029a3f6f 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -1,11 +1,22 @@ -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, UserManager from django.db import models -from rest_framework import serializers from backend.pigeonhole.apps.courses.models import Course +class Roles(models.IntegerChoices): + ADMIN = 1 + TEACHER = 2 + STUDENT = 3 + + class User(AbstractUser): + id = models.BigAutoField(primary_key=True) + course = models.ManyToManyField(Course) + role = models.IntegerField(choices=Roles.choices, default=Roles.STUDENT) + + objects = UserManager() + class Meta(AbstractUser.Meta): db_table = "auth_user" @@ -13,37 +24,14 @@ class Meta(AbstractUser.Meta): def name(self): return f"{self.first_name.strip()} {self.last_name.strip()}" + @property + def is_admin(self): + return self.role == Roles.ADMIN -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ['id', 'e_mail', 'first_name', 'last_name'] - - -class Student(models.Model): - id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) - number = models.IntegerField() - course = models.ManyToManyField(Course) - - objects = models.Manager() - - -class StudentSerializer(serializers.ModelSerializer): - class Meta: - model = Student - fields = ['number', 'course', 'id'] - - -class Teacher(models.Model): - id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) - course = models.ManyToManyField(Course) - is_admin = models.BooleanField(default=False) - is_assistant = models.BooleanField(default=False) - - objects = models.Manager() - + @property + def is_teacher(self): + return self.role == Roles.TEACHER -class TeacherSerializer(serializers.ModelSerializer): - class Meta: - model = Teacher - fields = ['course', 'id', 'is_admin', 'is_assistent'] + @property + def is_student(self): + return self.role == Roles.STUDENT diff --git a/backend/pigeonhole/apps/users/serializers.py b/backend/pigeonhole/apps/users/serializers.py new file mode 100644 index 00000000..968c163e --- /dev/null +++ b/backend/pigeonhole/apps/users/serializers.py @@ -0,0 +1,29 @@ +from django.contrib.auth.models import update_last_login +from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework_simplejwt.settings import api_settings + +from backend.pigeonhole.apps.users.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'email', 'first_name', 'last_name', 'course', 'role'] + + +class LoginSerializer(TokenObtainPairSerializer): + + def validate(self, attrs): + data = super().validate(attrs) + + refresh = self.get_token(self.user) + + data['user'] = UserSerializer(self.user).data + data['refresh'] = str(refresh) + data['access'] = str(refresh.access_token) + + if api_settings.UPDATE_LAST_LOGIN: + update_last_login(None, self.user) + + return data diff --git a/backend/pigeonhole/apps/users/views.py b/backend/pigeonhole/apps/users/views.py new file mode 100644 index 00000000..1aa9dab5 --- /dev/null +++ b/backend/pigeonhole/apps/users/views.py @@ -0,0 +1,27 @@ +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from backend.pigeonhole.apps.users.models import User +from backend.pigeonhole.apps.users.serializers import UserSerializer + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def list(self, request, *args, **kwargs): + serializer = UserSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, *args, **kwargs): + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/pigeonhole/apps/users/viewsets.py b/backend/pigeonhole/apps/users/viewsets.py new file mode 100644 index 00000000..cc6d5322 --- /dev/null +++ b/backend/pigeonhole/apps/users/viewsets.py @@ -0,0 +1,24 @@ +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework_simplejwt.exceptions import TokenError, InvalidToken +from rest_framework_simplejwt.views import TokenObtainPairView + +from backend.pigeonhole.apps.users.serializers import LoginSerializer + + +class LoginViewSet(ModelViewSet, TokenObtainPairView): + serializer_class = LoginSerializer + permission_classes = (AllowAny,) + http_method_names = ['post'] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response(serializer.validated_data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/settings.py b/backend/pigeonhole/settings.py index 3c7a0902..3b0b5fee 100644 --- a/backend/pigeonhole/settings.py +++ b/backend/pigeonhole/settings.py @@ -48,6 +48,7 @@ 'backend.pigeonhole.apps.projects', 'backend.pigeonhole.apps.submissions', 'backend.pigeonhole.apps.groups', + 'drf_yasg', ] MIDDLEWARE = [ @@ -117,7 +118,12 @@ } REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'DEFAULT_PAGINATION_CLASS': ( + 'rest_framework.pagination.PageNumberPagination', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), 'PAGE_SIZE': 10 } @@ -162,3 +168,11 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITIONS': { + 'Basic': { + 'type': 'basic' + } + } +} diff --git a/backend/pigeonhole/tests/test_models/test_conditions.py b/backend/pigeonhole/tests/test_models/test_conditions.py index 37307ab7..33544689 100644 --- a/backend/pigeonhole/tests/test_models/test_conditions.py +++ b/backend/pigeonhole/tests/test_models/test_conditions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project, Conditions, AllowedExtension, ForbiddenExtension @@ -22,8 +22,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -34,6 +34,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description" ) @@ -41,7 +42,6 @@ def setUp(self): self.conditions = Conditions.objects.create( submission_id=project, condition="Condition 1", - deadline="2021-12-12 12:12:12", test_file_location="path/to/test", test_file_type="txt" ) diff --git a/backend/pigeonhole/tests/test_models/test_course.py b/backend/pigeonhole/tests/test_models/test_course.py index 2498a217..d576e67f 100644 --- a/backend/pigeonhole/tests/test_models/test_course.py +++ b/backend/pigeonhole/tests/test_models/test_course.py @@ -1,6 +1,6 @@ from django.test import TestCase from backend.pigeonhole.apps.courses.models import Course -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from django.db.utils import DataError @@ -25,8 +25,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -34,7 +34,7 @@ def setUp(self): student.course.add(course) def test_course_teacher_relationship(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") course = Course.objects.get(name="Math") self.assertIn(course, teacher.course.all()) course_alter_ego = teacher.course.get(name="Math") @@ -42,7 +42,7 @@ def test_course_teacher_relationship(self): self.assertTrue(course_alter_ego, "Mathematics") def test_course_students_relationship(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") course = Course.objects.get(name="Math") self.assertIn(course, student.course.all()) course_alter_ego = student.course.get(name="Math") diff --git a/backend/pigeonhole/tests/test_models/test_groups.py b/backend/pigeonhole/tests/test_models/test_groups.py index 0ece53b9..0ca1335b 100644 --- a/backend/pigeonhole/tests/test_models/test_groups.py +++ b/backend/pigeonhole/tests/test_models/test_groups.py @@ -1,6 +1,6 @@ from django.test import TestCase from backend.pigeonhole.apps.courses.models import Course -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.groups.models import Group from backend.pigeonhole.apps.projects.models import Project @@ -34,9 +34,9 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) - student2 = Student.objects.create(id=student_user2, number=5678) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) + student2 = User.objects.create(id=student_user2, number=5678) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -47,6 +47,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) @@ -67,8 +68,8 @@ def test_group_project_relation(self): def test_group_student_relation(self): group = Group.objects.get(group_nr=1) - student = Student.objects.get(id__email="student@gmail.com") - student2 = Student.objects.get(id__email="student2@gmail.com") + student = User.objects.get(id__email="student@gmail.com") + student2 = User.objects.get(id__email="student2@gmail.com") self.assertIn(student, group.student.all()) self.assertIn(student2, group.student.all()) diff --git a/backend/pigeonhole/tests/test_models/test_project.py b/backend/pigeonhole/tests/test_models/test_project.py index 11285c91..99f75f5f 100644 --- a/backend/pigeonhole/tests/test_models/test_project.py +++ b/backend/pigeonhole/tests/test_models/test_project.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project @@ -22,8 +22,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -34,6 +34,7 @@ def setUp(self): self.project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) @@ -41,11 +42,11 @@ def test_project_course_relation(self): self.assertEqual(self.project.course_id.name, "Math") def test_project_teacher_relation(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertIn(self.project.course_id, teacher.course.all()) def test_project_student_relation(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertIn(self.project.course_id, student.course.all()) def test_course_name_length_validation(self): diff --git a/backend/pigeonhole/tests/test_models/test_submissions.py b/backend/pigeonhole/tests/test_models/test_submissions.py index c6efed08..698b012d 100644 --- a/backend/pigeonhole/tests/test_models/test_submissions.py +++ b/backend/pigeonhole/tests/test_models/test_submissions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project from backend.pigeonhole.apps.submissions.models import Submissions @@ -25,8 +25,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -37,6 +37,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) @@ -51,7 +52,6 @@ def setUp(self): # Create submission Submissions.objects.create( group_id=group, - submission_nr=1, ) def test_submission_student_relation(self): @@ -66,17 +66,6 @@ def test_submission_project_relation(self): project = submission.group_id.project_id self.assertEqual(submission.group_id.project_id, project) - def test_update_and_delete_submission(self): - submission = Submissions.objects.get(submission_nr=1) - submission.submission_nr = 2 - submission.save() - updated_submission = Submissions.objects.get(submission_nr=2) - self.assertEqual(updated_submission.submission_nr, 2) - - submission.delete() - with self.assertRaises(Submissions.DoesNotExist): - Submissions.objects.get(submission_nr=2) - def test_submission_file_upload_and_retrieval(self): submission = Submissions.objects.get(submission_nr=1) diff --git a/backend/pigeonhole/tests/test_models/test_user.py b/backend/pigeonhole/tests/test_models/test_user.py index 043197e5..383933fd 100644 --- a/backend/pigeonhole/tests/test_models/test_user.py +++ b/backend/pigeonhole/tests/test_models/test_user.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User # python3 manage.py test backend/ @@ -22,51 +22,51 @@ def setUp(self): ) # Create teacher and student using the created users - Teacher.objects.create(id=teacher_user) - Student.objects.create(id=student_user, number=1234) + User.objects.create(id=teacher_user) + User.objects.create(id=student_user, number=1234) def test_student(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertEqual(student.id.email, "student@gmail.com") self.assertEqual(student.number, 1234) # update student number student.number = 5678 student.save() - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertEqual(student.number, 5678) # delete student student.delete() - with self.assertRaises(Student.DoesNotExist): - Student.objects.get(id__email="student@gmail.com") + with self.assertRaises(User.DoesNotExist): + User.objects.get(id__email="student@gmail.com") def test_teacher(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.id.email, "teacher@gmail.com") self.assertEqual(teacher.is_admin, False) # update teacher is_admin teacher.is_admin = True teacher.save() - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.is_admin, True) self.assertEqual(teacher.is_assistant, False) # update teacher is_assistent teacher.is_assistant = True teacher.save() - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.is_assistant, True) # delete teacher teacher.delete() - with self.assertRaises(Teacher.DoesNotExist): - Teacher.objects.get(id__email="teacher@gmail.com") + with self.assertRaises(User.DoesNotExist): + User.objects.get(id__email="teacher@gmail.com") def test_create_student_without_user(self): with self.assertRaises(Exception): - Student.objects.create(number=1234) + User.objects.create(number=1234) def test_create_teacher_without_user(self): with self.assertRaises(Exception): - Teacher.objects.create(is_admin=True, is_assistent=True) + User.objects.create(is_admin=True, is_assistent=True) diff --git a/backend/pigeonhole/tests/test_views/__init__.py b/backend/pigeonhole/tests/test_views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_complete/admin.py b/backend/pigeonhole/tests/test_views/test_complete/admin.py new file mode 100644 index 00000000..da4d7afd --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_complete/admin.py @@ -0,0 +1,90 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User +from backend.pigeonhole.apps.submissions.models import Submission + +ROUTES_PREFIX = '/courses/' + + +class CompleteTestAdmin(TestCase): + def setUp(self): + self.client = APIClient() + + # Create a teacher user + self.teacher = User.objects.create( + username="teacher_username", + email="teacher@gmail.com", + first_name="Teacher", + last_name="LastName", + role=2 # Teacher role + ) + + # Create a student user + self.student = User.objects.create( + username="student_username", + email="student@gmail.com", + first_name="Student", + last_name="LastName", + role=3 # Student role + ) + + # Authenticate the teacher user + self.client.force_authenticate(self.teacher) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + def test_create_course(self): + # Use the teacher to create the course + response = self.client.post( + ROUTES_PREFIX, + { + "name": "Test Course 2", + "description": "Test Course 2 Description", + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 2) + self.assertEqual(Course.objects.get(course_id=2).name, "Test Course 2") + + def test_create_project(self): + # Use the teacher to create the project for the course + response = self.client.post( + ROUTES_PREFIX + f'{self.course.course_id}/projects/', + { + "name": "Test Project", + "description": "Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Project.objects.count(), 1) + self.assertEqual(Project.objects.get(project_id=1).name, "Test Project") + + def test_create_submission(self): + # Authenticate the student user + self.client.force_authenticate(self.student) + + # Use the student to create the submission for the project + response = self.client.post( + ROUTES_PREFIX + f'{self.course.course_id}/projects/1/submissions/', + { + "project_id": 1, + "student_id": self.student.id, # Use the student's id + "submission": "Test Submission", + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Submission.objects.count(), 1) + self.assertEqual(Submission.objects.get(submission_id=1).submission, "Test Submission") + + \ No newline at end of file diff --git a/backend/pigeonhole/tests/test_views/test_course/__init__.py b/backend/pigeonhole/tests/test_views/test_course/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_course/course.txt b/backend/pigeonhole/tests/test_views/test_course/course.txt new file mode 100644 index 00000000..cfcdd187 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/course.txt @@ -0,0 +1,91 @@ + +class CourseTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + + # Create a regular user (teacher) + self.user = User.objects.create_user( + username="teacher_username", + email="kermit@gmail.com", + first_name="Kermit", + last_name="The Frog" + ) + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + # Provide a value for the "number" field when creating the Student instance + self.student = Student.objects.create(id=self.user, number=123456) + self.student.course.set([self.course]) + + # Authenticate the test client with the regular user + self.client.force_authenticate(user=self.user) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + +class CourseTestUnauthorized(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(len(response.data), Course.objects.count()) +""" \ No newline at end of file diff --git a/backend/pigeonhole/tests/test_views/test_course/test_admin.py b/backend/pigeonhole/tests/test_views/test_course/test_admin.py new file mode 100644 index 00000000..53e66a39 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/test_admin.py @@ -0,0 +1,77 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class CourseTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") + + self.course = Course.objects.create(**self.course_data) + + # Create a regular user (teacher) + self.teacher = User.objects.create( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=1, + ) + self.teacher.course.set([self.course]) + self.client.force_authenticate(user=self.teacher) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course_not_of_teacher.refresh_from_db() + self.assertEqual(self.course_not_of_teacher.name, updated_data['name']) + self.assertEqual(self.course_not_of_teacher.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Course.objects.count()) diff --git a/backend/pigeonhole/tests/test_views/test_course/test_teacher.py b/backend/pigeonhole/tests/test_views/test_course/test_teacher.py new file mode 100644 index 00000000..dd99f6a3 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/test_teacher.py @@ -0,0 +1,82 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +# TODO + +class CourseTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") + + self.teacher = User.objects.create( + username="teacher_username", + email="teacher@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=2 + ) + + self.teacher.course.set([self.course]) + + self.client.force_authenticate(user=self.teacher) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Course.objects.count()) + + def test_retrieve_course(self): + response = self.client.get(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['name'], self.course.name) + self.assertEqual(response.data['description'], self.course.description) diff --git a/backend/pigeonhole/tests/test_views/test_group/__init__.py b/backend/pigeonhole/tests/test_views/test_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_project/__init__.py b/backend/pigeonhole/tests/test_views/test_project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_project/test_admin.py b/backend/pigeonhole/tests/test_views/test_project/test_admin.py new file mode 100644 index 00000000..474bd2fe --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_admin.py @@ -0,0 +1,189 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.admin = User.objects.create( + username="admin_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=1 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.admin.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.admin) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Project.objects.count(), 2) + self.assertEqual(Project.objects.get(project_id=2).name, "Test Project 2") + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.count(), 1) + + def test_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_delete_project_invalid_course(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.count(), 1) + + def test_partial_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_partial_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_student.py b/backend/pigeonhole/tests/test_views/test_project/test_student.py new file mode 100644 index 00000000..438f9ed5 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_student.py @@ -0,0 +1,190 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + self.student = User.objects.create( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=3 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_student = Course.objects.create( + name="Test Course 2", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.student) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the student + + def test_create_project_course_not_of_student(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_course_not_of_student(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_teacher.py b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py new file mode 100644 index 00000000..c2e565f7 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py @@ -0,0 +1,181 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + self.teacher = User.objects.create( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=2 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_teacher = Course.objects.create( + name="Test Course 2", + ) + + self.teacher.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.teacher) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Updated assertion to use the correct project_id from the response + created_project_id = response.data.get('project_id') + self.assertEqual(Project.objects.get(project_id=created_project_id).name, "Test Project 2") + self.assertEqual(Project.objects.count(), 2) + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the teacher + + def test_create_project_course_not_of_teacher(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_teacher.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_project_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_project_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py new file mode 100644 index 00000000..4d54ae51 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py @@ -0,0 +1,177 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestUnauthenticated(TestCase): + def setUp(self): + self.client = APIClient() + self.student = User.objects.create( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=3 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + def test_create_project_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with an invalid course + + def test_create_project_invalid_course_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_invalid_course_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/tests/test_views/test_submission/__init__.py b/backend/pigeonhole/tests/test_views/test_submission/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 2cb677dd..7e1c4468 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,17 +1,48 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin from django.urls import include, path -from rest_framework import routers +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import routers, permissions +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from backend.testapi import views +from backend.pigeonhole.apps.courses.views import CourseViewSet +from backend.pigeonhole.apps.groups.views import GroupViewSet +from backend.pigeonhole.apps.projects.views import ProjectViewSet +from backend.pigeonhole.apps.submissions.views import SubmissionsViewset +from backend.pigeonhole.apps.users.views import UserViewSet + +schema_view = get_schema_view( + openapi.Info( + title="My API", + default_version='v1', + description="My API description", + terms_of_service="https://www.example.com/terms/", + contact=openapi.Contact(email="contact@example.com"), + license=openapi.License(name="Awesome License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) router = routers.DefaultRouter() -router.register(r'users', views.UserViewSet) -router.register(r'groups', views.GroupViewSet) +router.register(r'users', UserViewSet) +router.register(r'courses', CourseViewSet) +router.register(r'submissions', SubmissionsViewset) +router.register(r'courses/(?P[^/.]+)/projects', ProjectViewSet) +router.register(r'courses/(?P[^/.]+)/projects/(?P[^/.]+)/groups', GroupViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ - path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) -] + path('', include(router.urls)), + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path("admin/", admin.site.urls), + path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('refresh/', TokenRefreshView.as_view(), name='token_refresh'), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += router.urls diff --git a/backend/requirements.txt b/backend/requirements.txt index df59d6c2..22eb5e0d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,4 +5,7 @@ djangorestframework-simplejwt~=5.2.2 djangorestframework~=3.14.0 flake8==7.0.0 psycopg2-binary~=2.9.5 -pytz~=2022.7.1 \ No newline at end of file +pytz~=2022.7.1 +pyyaml==6.0.1 +uritemplate==4.1.1 +drf-yasg==1.21.7 diff --git a/backend/runtests.sh b/backend/runtests.sh new file mode 100644 index 00000000..65d41fa8 --- /dev/null +++ b/backend/runtests.sh @@ -0,0 +1 @@ +coverage run manage.py test backend/pigeonhole/tests/test_views \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index c4033664..08e12e04 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,7 +16,7 @@ bun dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +You can start editing the page by modifying `app/index.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d72dd96b..4aee891d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,15 +8,28 @@ "name": "pigeonhole", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.12", + "@mui/icons-material": "^5.15.12", + "@mui/material": "^5.15.12", + "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.6.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.344.0", "next": "14.1.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "http-proxy-middleware": "^2.0.6", "eslint": "^8", "eslint-config-next": "14.1.0", "postcss": "^8", @@ -37,7 +50,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "engines": { "node": ">=10" }, @@ -45,11 +57,190 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/runtime": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -57,6 +248,152 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -113,6 +450,45 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@fontsource/roboto": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.12.tgz", + "integrity": "sha512-x0o17jvgoSSbS9OZnUX2+xJmVRvVCfeaYJjkS7w62iN7CuJWtMf5vJj8LqgC7ibqIkitOHVW+XssRjgrcHn62g==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -150,7 +526,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -167,7 +542,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -179,7 +553,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -194,7 +567,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -208,7 +580,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -217,7 +588,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -225,19 +595,272 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.38.tgz", + "integrity": "sha512-AsjD6Y1X5A1qndxz8xCcR8LDqv31aiwlgWMPxFAX/kCKiIGKlK65yMeVZ62iQr/6LBz+9hSKLiD1i4TZdAHKcQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.12.tgz", + "integrity": "sha512-brRO+tMFLpGyjEYHrX97bzqeF6jZmKpqqe1rY0LyIHAwP6xRVzh++zSecOQorDOCaZJg4XkGT9xfD+RWOWxZBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.12.tgz", + "integrity": "sha512-3BXiDlOd3AexZoEXa/VqpIpVIvosCzjLHsdMWzKMXbZdnBiJjmb9ECdqfjn5SpTClO49qvkKLhkTqdBH3fSFGw==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.12.tgz", + "integrity": "sha512-vXJGg6KNKucsvbW6l7w9zafnpOp0CWc0Wx4mDykuABTpQ5QQBnZxP7+oB4yAS1hDZQ1WobbeIl0CjxK4EEahkA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.38", + "@mui/core-downloads-tracker": "^5.15.12", + "@mui/system": "^5.15.12", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.12.tgz", + "integrity": "sha512-cqoSo9sgA5HE+8vZClbLrq9EkyOnYysooepi5eKaKvJ41lReT2c5wOZAeDDM1+xknrMDos+0mT2zr3sZmUiRRA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.12", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz", + "integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.12.tgz", + "integrity": "sha512-/pq+GO6yN3X7r3hAwFTrzkAh7K1bTF5r8IzS79B9eyKJg7v6B/t4/zZYMR6OT9qEPtwf6rYN2Utg1e6Z7F1OgQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.12", + "@mui/styled-engine": "^5.15.11", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.12.tgz", + "integrity": "sha512-8SDGCnO2DY9Yy+5bGzu00NZowSDtuyHP4H8gunhHGQoIlhlY2Z3w64wBzAOLpYw/ZhJNzksDTnS/i8qdJvxuow==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/@next/env": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", @@ -391,7 +1014,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -404,7 +1026,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -413,7 +1034,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -426,12 +1046,55 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", @@ -446,6 +1109,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -456,22 +1127,24 @@ "version": "20.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.57", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.57.tgz", "integrity": "sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -487,11 +1160,18 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", @@ -667,7 +1347,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -676,7 +1355,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -690,14 +1368,12 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -709,8 +1385,7 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -895,6 +1570,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.17", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", @@ -956,6 +1636,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -965,17 +1655,29 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } @@ -994,7 +1696,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1068,7 +1769,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1077,7 +1777,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "engines": { "node": ">= 6" } @@ -1121,7 +1820,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1145,7 +1843,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1153,16 +1850,42 @@ "node": ">= 6" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1173,14 +1896,23 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "engines": { "node": ">= 6" } @@ -1191,11 +1923,38 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1209,7 +1968,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -1220,8 +1978,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1286,6 +2043,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1298,8 +2063,7 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -1316,8 +2080,7 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/doctrine": { "version": "3.0.0", @@ -1331,11 +2094,19 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { "version": "1.4.676", @@ -1346,8 +2117,7 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/enhanced-resolve": { "version": "5.15.0", @@ -1362,6 +2132,14 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", @@ -1523,7 +2301,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -1941,6 +2718,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1951,7 +2733,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1967,7 +2748,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1991,7 +2771,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -2012,7 +2791,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2020,6 +2798,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2056,6 +2839,25 @@ "integrity": "sha512-noqGuLw158+DuD9UPRKHpJ2hGxpFyDlYYrfM0mWt4XhT4n0lwzTLh70Tkdyy4kyTmyTT9Bv7bWAJqw7cgkEXDg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2069,7 +2871,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -2081,6 +2882,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2104,7 +2918,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2118,7 +2931,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2202,7 +3014,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -2224,7 +3035,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -2236,7 +3046,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2245,7 +3054,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2402,7 +3210,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2410,6 +3217,50 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2423,7 +3274,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2490,6 +3340,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2521,7 +3376,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2561,7 +3415,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -2588,7 +3441,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2609,7 +3461,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -2633,7 +3484,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2666,7 +3516,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -2695,6 +3544,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -2820,8 +3680,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/iterator.prototype": { "version": "1.1.2", @@ -2840,7 +3699,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2858,7 +3716,6 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, "bin": { "jiti": "bin/jiti.js" } @@ -2886,6 +3743,11 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2969,7 +3831,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, "engines": { "node": ">=10" } @@ -2977,8 +3838,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -3016,16 +3876,22 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.344.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", + "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -3034,7 +3900,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -3043,6 +3908,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3068,7 +3952,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -3083,7 +3966,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -3195,7 +4077,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3213,7 +4094,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3222,7 +4102,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -3397,7 +4276,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -3405,6 +4283,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3427,7 +4322,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -3435,14 +4329,12 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3458,7 +4350,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -3472,7 +4363,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -3484,7 +4374,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3493,7 +4382,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "engines": { "node": ">= 6" } @@ -3511,7 +4399,6 @@ "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3539,7 +4426,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -3556,7 +4442,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -3575,7 +4460,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3610,7 +4494,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "dev": true, "engines": { "node": ">=14" }, @@ -3622,7 +4505,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -3641,7 +4523,6 @@ "version": "6.0.15", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dev": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3653,8 +4534,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -3669,13 +4549,17 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3689,7 +4573,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -3731,14 +4614,27 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -3747,7 +4643,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -3779,8 +4674,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -3800,11 +4694,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -3821,7 +4719,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -3839,7 +4736,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -3884,7 +4780,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -4009,7 +4904,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4021,7 +4915,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -4048,7 +4941,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -4065,6 +4957,14 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4085,7 +4985,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -4103,7 +5002,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4116,14 +5014,12 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -4135,7 +5031,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4215,7 +5110,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4228,7 +5122,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4279,11 +5172,15 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -4317,7 +5214,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4325,11 +5221,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4362,6 +5269,14 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -4381,7 +5296,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -4390,7 +5304,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -4398,11 +5311,18 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4425,8 +5345,7 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -4566,8 +5485,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -4611,14 +5529,12 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -4709,7 +5625,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -4727,7 +5642,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4743,14 +5657,12 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4764,7 +5676,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -4776,7 +5687,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -4788,7 +5698,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4815,7 +5724,6 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, "engines": { "node": ">= 14" } diff --git a/frontend/package.json b/frontend/package.json index 47c304b2..9d6b7c12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,19 +9,32 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.12", + "@mui/icons-material": "^5.15.12", + "@mui/material": "^5.15.12", + "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.6.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "http-proxy-middleware": "^2.0.6", + "lucide-react": "^0.344.0", + "next": "14.1.0", "react": "^18", "react-dom": "^18", - "next": "14.1.0" + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.0", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.0" + "typescript": "^5" } } diff --git a/frontend/pages/_document.js b/frontend/pages/_document.js new file mode 100644 index 00000000..c732e018 --- /dev/null +++ b/frontend/pages/_document.js @@ -0,0 +1,26 @@ +import Document, {Head, Html, Main, NextScript} from 'next/document'; +import {PRIMARY_COLOR} from "../src/lib/utils"; + +class MyDocument extends Document { + render() { + return ( + + + {/* PWA primary color */} + + + {/* Add other head elements here as needed */} + + +
+ + + + ); + } +} + +export default MyDocument; diff --git a/frontend/src/app/components/CASButton.tsx b/frontend/src/app/components/CASButton.tsx new file mode 100644 index 00000000..6af7e5c7 --- /dev/null +++ b/frontend/src/app/components/CASButton.tsx @@ -0,0 +1,27 @@ +"use client"; +import React from 'react' +import SchoolIcon from "@mui/icons-material/School"; +import {Button} from "@mui/material"; + +const CASButton = () => { + const handleCASLogin = (): void => { + // Implement CAS login logic here + console.log('Login with CAS'); + }; + + return ( +
+ +
+ ) +} + +export default CASButton diff --git a/frontend/src/app/components/LoginCard.tsx b/frontend/src/app/components/LoginCard.tsx new file mode 100644 index 00000000..af1b2b86 --- /dev/null +++ b/frontend/src/app/components/LoginCard.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {Box, Container, CssBaseline, Typography} from '@mui/material'; +import {ThemeProvider} from '@mui/material/styles'; +import CASButton from "@/app/components/CASButton"; +import LoginForm from "@/app/components/LoginForm"; +import loginTheme from '../../styles/theme'; + +const LoginCard: React.FC = () => { + return ( + + + + + + Pigeonhole + +
+ + + + OR + + + +
+
+
+
+ ); +}; + +export default LoginCard; diff --git a/frontend/src/app/components/LoginForm.tsx b/frontend/src/app/components/LoginForm.tsx new file mode 100644 index 00000000..de1db578 --- /dev/null +++ b/frontend/src/app/components/LoginForm.tsx @@ -0,0 +1,68 @@ +"use client"; +import React, {useState} from 'react'; +import {Button, IconButton, InputAdornment, TextField} from "@mui/material"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import AuthService from "../../authentication/auth"; + +const LoginForm = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const handleLogin = (): void => { + // Implement your login logic here + AuthService.login(username, password).then((data) => { + console.log("Logged in") + console.log(data) + }) + }; + + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleMouseDownPassword = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( +
+ setUsername(e.target.value)} + fullWidth + /> + setPassword(e.target.value)} + fullWidth + InputProps={{ + endAdornment: ( + + + {showPassword ? ( + // Set the icon to small + ) : ( + // Set the icon to small + )} + + + ), + }} + /> + +
+ ); +} + +export default LoginForm; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css deleted file mode 100644 index 875c01e8..00000000 --- a/frontend/src/app/globals.css +++ /dev/null @@ -1,33 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 3314e478..a14e64fc 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,22 +1,16 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} export default function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: { + children: React.ReactNode +}) { return ( - {children} + {children} - ); + ) } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ee07830a..66d47abf 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,37 +1,13 @@ -"use client" +import React from 'react'; +import LoginCard from '@/app/components/LoginCard'; -import Image from "next/image"; -import React, { useState, useEffect } from 'react'; - -export default function Home() { - - - const [data, setData] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('http://127.0.0.1:8000/groups/'); - if (response.ok) { - const json = await response.json(); - setData(json); - } else { - console.error('Failed to fetch data'); - } - } catch (error) { - console.error('Error:', error); - } - }; - - fetchData(); - }, []); - - - return ( -
-

Deployment test:

-

{JSON.stringify(data)}

-
- ); +const Login = () => { + return ( +
+ +
+ ) } + +export default Login diff --git a/frontend/src/authentication/auth-header.js b/frontend/src/authentication/auth-header.js new file mode 100644 index 00000000..f1faaaf9 --- /dev/null +++ b/frontend/src/authentication/auth-header.js @@ -0,0 +1,10 @@ +function authHeader() { + const user = JSON.parse(localStorage.getItem('user')); + if (user && user.accessToken) { + return {Authorization: 'Bearer ' + user.accessToken}; + } else { + return {}; + } +} + +export default authHeader; diff --git a/frontend/src/authentication/auth.js b/frontend/src/authentication/auth.js new file mode 100644 index 00000000..a11b2345 --- /dev/null +++ b/frontend/src/authentication/auth.js @@ -0,0 +1,28 @@ +import axios from "axios"; + +const API_URL = "/api/"; + +class Auth { + + login(username, password) { + return axios + .post("http://127.0.0.1:8000/login/", { + username, + password + }) + .then(response => { + console.log(response) + if (response.data.access) { + localStorage.setItem("user", JSON.stringify(response.data.user)); + } + return response.data; + }); + } + + getCurrentUser() { + return JSON.parse(localStorage.getItem('user')); + } + +} + +export default new Auth(); \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 00000000..c8ec6dab --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,8 @@ +import {type ClassValue, clsx} from "clsx" +import {twMerge} from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export const PRIMARY_COLOR = "#1976d2"; diff --git a/frontend/src/proxy.js b/frontend/src/proxy.js new file mode 100644 index 00000000..52198015 --- /dev/null +++ b/frontend/src/proxy.js @@ -0,0 +1,11 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function(app) { + app.use( + '/api', + createProxyMiddleware({ + target: 'http://localhost:8000/', + changeOrigin: true, + }) + ); +}; \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css new file mode 100644 index 00000000..6a757250 --- /dev/null +++ b/frontend/src/styles/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts new file mode 100644 index 00000000..3ff4e7f9 --- /dev/null +++ b/frontend/src/styles/theme.ts @@ -0,0 +1,44 @@ +"use client"; +import { createTheme } from '@mui/material/styles'; + +const loginTheme = createTheme({ + palette: { + background: { + default: '#f4f5fd' + }, + primary: { + main: '#1976d2', + }, + secondary: { + main: '#9c27b0', + }, + }, + typography: { + fontFamily: 'Quicksand, sans-serif', + h4: { + fontWeight: 700, + }, + }, + components: { + MuiTextField: { + defaultProps: { + InputLabelProps: { + shrink: true, + }, + margin: 'normal', + required: true, + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + variant: 'contained', + color: 'primary', + fullWidth: true, + style: {margin: '10px 0'}, + }, + }, + }, +}); + +export default loginTheme; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index e9a0944e..84287e82 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,20 +1,80 @@ -import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss" -const config: Config = { +const config = { + darkMode: ["class"], content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, - plugins: [], -}; -export default config; + plugins: [require("tailwindcss-animate")], +} satisfies Config + +export default config \ No newline at end of file