diff --git a/backend/boards/__init__.py b/backend/boards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/boards/admin.py b/backend/boards/admin.py new file mode 100644 index 0000000..c22f595 --- /dev/null +++ b/backend/boards/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from .models import Board, ListBoard + +@admin.register(Board) +class BoardAdmin(admin.ModelAdmin): + list_display = ['name', 'user'] + +@admin.register(ListBoard) +class ListBoardAdmin(admin.ModelAdmin): + list_display = ['name', 'position', 'board'] + list_filter = ['board', 'position'] \ No newline at end of file diff --git a/backend/boards/apps.py b/backend/boards/apps.py new file mode 100644 index 0000000..d10d8fa --- /dev/null +++ b/backend/boards/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class BoardsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'boards' + + def ready(self): + import boards.signals \ No newline at end of file diff --git a/backend/boards/migrations/0001_initial.py b/backend/boards/migrations/0001_initial.py new file mode 100644 index 0000000..2196132 --- /dev/null +++ b/backend/boards/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.6 on 2023-11-19 19:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Board', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ListBoard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('position', models.IntegerField()), + ('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boards.board')), + ], + ), + ] diff --git a/backend/boards/migrations/__init__.py b/backend/boards/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/boards/models.py b/backend/boards/models.py new file mode 100644 index 0000000..de2d107 --- /dev/null +++ b/backend/boards/models.py @@ -0,0 +1,34 @@ +from django.db import models + +from users.models import CustomUser + +class Board(models.Model): + """ + Kanban board model. + + :param user: The user who owns the board. + :param name: The name of the board. + :param created_at: The date and time when the board was created. + """ + user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return f"{self.name}" + + +class ListBoard(models.Model): + """ + List inside a Kanban board. + + :param board: The board that the list belongs to. + :param name: The name of the list. + :param position: The position of the list in Kanban. + """ + board = models.ForeignKey(Board, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + position = models.IntegerField() + + def __str__(self) -> str: + return f"{self.name}" diff --git a/backend/boards/signals.py b/backend/boards/signals.py new file mode 100644 index 0000000..87861f1 --- /dev/null +++ b/backend/boards/signals.py @@ -0,0 +1,14 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from boards.models import Board, ListBoard +from users.models import CustomUser + +@receiver(post_save, sender=CustomUser) +def create_default_board(sender, instance, created, **kwargs): + if created: + board = Board.objects.create(user=instance, name="My Default Board") + + ListBoard.objects.create(board=board, name="Todo", position=1) + ListBoard.objects.create(board=board, name="In Progress", position=2) + ListBoard.objects.create(board=board, name="Done", position=3) \ No newline at end of file diff --git a/backend/boards/tests.py b/backend/boards/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/boards/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/boards/urls.py b/backend/boards/urls.py new file mode 100644 index 0000000..d2d839f --- /dev/null +++ b/backend/boards/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + +] diff --git a/backend/boards/views.py b/backend/boards/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/boards/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/core/settings.py b/backend/core/settings.py index 5c01d05..9e7903c 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -52,6 +52,8 @@ INSTALLED_APPS = [ 'tasks', 'users', 'authentications', + 'dashboard', + 'boards', 'corsheaders', 'drf_spectacular', diff --git a/backend/core/urls.py b/backend/core/urls.py index a02869c..3ba1133 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -27,4 +27,6 @@ urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('api/', include('dashboard.urls')), + path('api/', include('boards.urls')), ] \ No newline at end of file diff --git a/backend/dashboard/__init__.py b/backend/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/dashboard/admin.py b/backend/dashboard/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/dashboard/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/dashboard/apps.py b/backend/dashboard/apps.py new file mode 100644 index 0000000..7b1cc05 --- /dev/null +++ b/backend/dashboard/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dashboard' diff --git a/backend/dashboard/migrations/__init__.py b/backend/dashboard/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/dashboard/serializers.py b/backend/dashboard/serializers.py new file mode 100644 index 0000000..ddc207b --- /dev/null +++ b/backend/dashboard/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import UserStats + +class UserStatsSerializer(serializers.ModelSerializer): + class Meta: + model = UserStats + fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level'] \ No newline at end of file diff --git a/backend/dashboard/tests.py b/backend/dashboard/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/dashboard/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py new file mode 100644 index 0000000..beb8f1b --- /dev/null +++ b/backend/dashboard/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import DashboardStatsAPIView + +urlpatterns = [ + path('dashboard/stats/', DashboardStatsAPIView.as_view(), name='dashboard-stats'), +] diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py new file mode 100644 index 0000000..4475d77 --- /dev/null +++ b/backend/dashboard/views.py @@ -0,0 +1,58 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from django.db.models import Count +from django.utils import timezone + +from tasks.models import Todo, RecurrenceTask + +class DashboardStatsAPIView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + user = request.user + + # Calculate task usage statistics + todo_count = Todo.objects.filter(user=user).count() + recurrence_task_count = RecurrenceTask.objects.filter(user=user).count() + + # Calculate how many tasks were completed in the last 7 days + completed_todo_count_last_week = Todo.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() + completed_recurrence_task_count_last_week = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() + + # Calculate subtask completion rate + total_subtasks = Todo.objects.filter(user=user).aggregate(total=Count('subtask__id'))['total'] + completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True).aggregate(total=Count('subtask__id'))['total'] + + # Calculate overall completion rate + total_tasks = todo_count + recurrence_task_count + completed_tasks = completed_todo_count_last_week + completed_recurrence_task_count_last_week + overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0 + + data = { + 'todo_count': todo_count, + 'recurrence_task_count': recurrence_task_count, + 'completed_todo_count_last_week': completed_todo_count_last_week, + 'completed_recurrence_task_count_last_week': completed_recurrence_task_count_last_week, + 'total_subtasks': total_subtasks, + 'completed_subtasks': completed_subtasks, + 'overall_completion_rate': overall_completion_rate, + } + + return Response(data, status=status.HTTP_200_OK) + + def post(self, request): + # Handle incoming data from the POST request + # Update the necessary information based on the data + + task_id = request.data.get('task_id') + is_completed = request.data.get('is_completed') + + try: + task = Todo.objects.get(id=task_id, user=request.user) + task.completed = is_completed + task.save() + return Response({'message': 'Task completion status updated successfully'}, status=status.HTTP_200_OK) + except Todo.DoesNotExist: + return Response({'error': 'Task not found'}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/backend/sample.env b/backend/sample.env index 15b50eb..b186167 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -7,4 +7,7 @@ DB_PASSWORD=your_DB_PASSWORD DB_HOST=your_DB_HOST DB_PORT=your_DB_PORT GOOGLE_CLIENT_ID=your_GOOGLE_CLIENT_ID -GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET \ No newline at end of file +GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET +BUCKET_NAME=your_BUCKET_NAME +AMAZON_S3_ACCESS_KEY=YOUR_S3_ACCESS_KEY +AMAZON_S3_SECRET_ACCESS_KEY=YOUR_S3_SECRET diff --git a/backend/tasks/admin.py b/backend/tasks/admin.py index 8c38f3f..5c5bbb3 100644 --- a/backend/tasks/admin.py +++ b/backend/tasks/admin.py @@ -1,3 +1,29 @@ from django.contrib import admin +from .models import Tag, Todo, RecurrenceTask, RecurrencePattern, Habit, Subtask -# Register your models here. +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + list_display = ['name'] + +@admin.register(Todo) +class TodoAdmin(admin.ModelAdmin): + list_display = ['title', 'list_board', 'is_active', 'priority'] + list_filter = ['list_board', 'is_active', 'priority'] + +@admin.register(RecurrenceTask) +class RecurrenceTaskAdmin(admin.ModelAdmin): + list_display = ['title', 'list_board', 'rrule', 'is_active'] + list_filter = ['list_board', 'rrule', 'is_active'] + +@admin.register(RecurrencePattern) +class RecurrencePatternAdmin(admin.ModelAdmin): + list_display = ['recurrence_task', 'recurring_type', 'day_of_week', 'week_of_month', 'day_of_month', 'month_of_year'] + +@admin.register(Habit) +class HabitAdmin(admin.ModelAdmin): + list_display = ['title', 'streak', 'current_count'] + +@admin.register(Subtask) +class SubtaskAdmin(admin.ModelAdmin): + list_display = ['parent_task', 'description', 'completed'] + list_filter = ['parent_task', 'completed'] diff --git a/backend/tasks/api.py b/backend/tasks/api.py index 587f433..fdf2493 100644 --- a/backend/tasks/api.py +++ b/backend/tasks/api.py @@ -6,9 +6,9 @@ from rest_framework import viewsets from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from tasks.utils import get_service, generate_recurrence_rule -from tasks.models import Todo, RecurrenceTask -from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer +from tasks.utils import get_service +from tasks.models import Todo +from tasks.serializers import TodoUpdateSerializer class GoogleCalendarEventViewset(viewsets.ViewSet): """Viewset for list or save Google Calendar Events.""" @@ -50,7 +50,11 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): return events def _validate_serializer(self, serializer): - """Validate serializer and return response.""" + """ + Validate serializer and return response. + + :param serializer: The serializer to validate. + """ if serializer.is_valid(): serializer.save() return Response("Validate Successfully", status=200) @@ -61,7 +65,6 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): events = self._get_google_events(request) responses = [] - recurrence_task_ids = [] for event in events: start_datetime = event.get('start', {}).get('dateTime') end_datetime = event.get('end', {}).get('dateTime') @@ -71,25 +74,6 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): event.pop('start') event.pop('end') - if (event.get('recurringEventId') in recurrence_task_ids): - continue - - if (event.get('recurringEventId') is not None): - originalStartTime = event.get('originalStartTime', {}).get('dateTime') - rrule_text = generate_recurrence_rule(event['start_datetime'], event['end_datetime'], originalStartTime) - event['recurrence'] = rrule_text - event.pop('originalStartTime') - recurrence_task_ids.append(event['recurringEventId']) - - try: - task = RecurrenceTask.objects.get(google_calendar_id=event['id']) - serializer = RecurrenceTaskUpdateSerializer(instance=task, data=event) - except RecurrenceTask.DoesNotExist: - serializer = RecurrenceTaskUpdateSerializer(data=event, user=request.user) - - responses.append(self._validate_serializer(serializer)) - continue - try: task = Todo.objects.get(google_calendar_id=event['id']) serializer = TodoUpdateSerializer(instance=task, data=event) diff --git a/backend/tasks/migrations/0014_recurrencetask_completed_todo_completed.py b/backend/tasks/migrations/0014_recurrencetask_completed_todo_completed.py new file mode 100644 index 0000000..d89360d --- /dev/null +++ b/backend/tasks/migrations/0014_recurrencetask_completed_todo_completed.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-11-17 16:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0013_alter_recurrencetask_recurrence_rule'), + ] + + operations = [ + migrations.AddField( + model_name='recurrencetask', + name='completed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='todo', + name='completed', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/tasks/migrations/0015_recurrencepattern_remove_transaction_user_and_more.py b/backend/tasks/migrations/0015_recurrencepattern_remove_transaction_user_and_more.py new file mode 100644 index 0000000..ee9c4bc --- /dev/null +++ b/backend/tasks/migrations/0015_recurrencepattern_remove_transaction_user_and_more.py @@ -0,0 +1,107 @@ +# Generated by Django 4.2.6 on 2023-11-19 20:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0001_initial'), + ('tasks', '0014_recurrencetask_completed_todo_completed'), + ] + + operations = [ + migrations.CreateModel( + name='RecurrencePattern', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('recurring_type', models.IntegerField(choices=[(0, 'Daily'), (1, 'Weekly'), (2, 'Monthly'), (3, 'Yearly')])), + ('max_occurrences', models.IntegerField(default=0)), + ('day_of_week', models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])), + ('week_of_month', models.IntegerField(choices=[(1, 'First'), (2, 'Second'), (3, 'Third'), (4, 'Fourth'), (5, 'Last')])), + ('day_of_month', models.IntegerField(default=0)), + ('month_of_year', models.IntegerField(choices=[(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')])), + ], + ), + migrations.RemoveField( + model_name='transaction', + name='user', + ), + migrations.DeleteModel( + name='UserNotification', + ), + migrations.RemoveField( + model_name='habit', + name='end_event', + ), + migrations.RemoveField( + model_name='habit', + name='google_calendar_id', + ), + migrations.RemoveField( + model_name='habit', + name='start_event', + ), + migrations.RemoveField( + model_name='recurrencetask', + name='google_calendar_id', + ), + migrations.RemoveField( + model_name='recurrencetask', + name='recurrence_rule', + ), + migrations.AddField( + model_name='habit', + name='current_count', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='recurrencetask', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='recurrencetask', + name='is_full_day_event', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='recurrencetask', + name='list_board', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + migrations.AddField( + model_name='recurrencetask', + name='parent_task', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'), + ), + migrations.AddField( + model_name='recurrencetask', + name='rrule', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='todo', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='todo', + name='is_full_day_event', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='todo', + name='list_board', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + migrations.DeleteModel( + name='Transaction', + ), + migrations.AddField( + model_name='recurrencepattern', + name='recurrence_task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'), + ), + ] diff --git a/backend/tasks/migrations/0016_alter_recurrencetask_list_board_and_more.py b/backend/tasks/migrations/0016_alter_recurrencetask_list_board_and_more.py new file mode 100644 index 0000000..b0edc6e --- /dev/null +++ b/backend/tasks/migrations/0016_alter_recurrencetask_list_board_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.6 on 2023-11-19 20:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0001_initial'), + ('tasks', '0015_recurrencepattern_remove_transaction_user_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='recurrencetask', + name='list_board', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + migrations.AlterField( + model_name='todo', + name='list_board', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + ] diff --git a/backend/tasks/migrations/0017_alter_recurrencetask_list_board_and_more.py b/backend/tasks/migrations/0017_alter_recurrencetask_list_board_and_more.py new file mode 100644 index 0000000..dee431c --- /dev/null +++ b/backend/tasks/migrations/0017_alter_recurrencetask_list_board_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.6 on 2023-11-19 20:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0001_initial'), + ('tasks', '0016_alter_recurrencetask_list_board_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='recurrencetask', + name='list_board', + field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + migrations.AlterField( + model_name='todo', + name='list_board', + field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'), + ), + ] diff --git a/backend/tasks/models.py b/backend/tasks/models.py index a8fc4e5..fc29fe6 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -1,6 +1,8 @@ from django.db import models from django.conf import settings +from boards.models import ListBoard, Board + class Tag(models.Model): """ Represents a tag that can be associated with tasks. @@ -12,22 +14,17 @@ class Tag(models.Model): class Task(models.Model): """ - Represents a Abstract of task, such as Habit, Daily, Todo, or Reward. + Represents a Abstract of task, such as Habit, Recurrence, Todo. :param user: The user who owns the task. :param title: Title of the task. :param notes: Optional additional notes for the task. :param tags: Associated tags for the task. - :param completed: A boolean field indicating whether the task is completed. :param importance: The importance of the task (range: 1 to 5) :param difficulty: The difficulty of the task (range: 1 to 5). :param challenge: Associated challenge (optional). :param fromSystem: A boolean field indicating if the task is from System. :param creation_date: Creation date of the task. - :param last_update: Last updated date of the task. - :param: google_calendar_id: Google Calendar Event ID of the task. - :param start_event: Start event of the task. - :param end_event: End event(Due Date) of the task. """ class Difficulty(models.IntegerChoices): EASY = 1, 'Easy' @@ -46,36 +43,132 @@ class Task(models.Model): fromSystem = models.BooleanField(default=False) creation_date = models.DateTimeField(auto_now_add=True) last_update = models.DateTimeField(auto_now=True) - google_calendar_id = models.CharField(max_length=255, null=True, blank=True) - start_event = models.DateTimeField(null=True) - end_event = models.DateTimeField(null=True) class Meta: abstract = True class Todo(Task): - + """ + Represent a Todo task. + + :param list_board: The list board that the task belongs to. + :param is_active: A boolean field indicating whether the task is active. (Archive or not) + :param is_full_day_event: A boolean field indicating whether the task is a full day event. + :param start_event: Start date and time of the task. + :param end_event: End date and time of the task. + :param google_calendar_id: The Google Calendar ID of the task. + :param completed: A boolean field indicating whether the task is completed. + :param priority: The priority of the task (range: 1 to 4). + """ class EisenhowerMatrix(models.IntegerChoices): IMPORTANT_URGENT = 1, 'Important & Urgent' IMPORTANT_NOT_URGENT = 2, 'Important & Not Urgent' NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent' NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent' + list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1) + is_active = models.BooleanField(default=True) + is_full_day_event = models.BooleanField(default=False) + start_event = models.DateTimeField(null=True) + end_event = models.DateTimeField(null=True) + google_calendar_id = models.CharField(max_length=255, null=True, blank=True) + completed = models.BooleanField(default=False) priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) def __str__(self): return self.title class RecurrenceTask(Task): - recurrence_rule = models.CharField() + """ + Represent a Recurrence task. (Occure every day, week, month, year) + + :param list_board: The list board that the task belongs to. + :param rrule: The recurrence rule of the task. + :param is_active: A boolean field indicating whether the task is active. (Archive or not) + :param is_full_day_event: A boolean field indicating whether the task is a full day event. + :param start_event: Start date and time of the task. + :param end_event: End date and time of the task. + :param completed: A boolean field indicating whether the task is completed. + :param parent_task: The parent task of the subtask. + """ + list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1) + rrule = models.CharField(max_length=255, null=True, blank=True) + is_active = models.BooleanField(default=True) + is_full_day_event = models.BooleanField(default=False) + start_event = models.DateTimeField(null=True) + end_event = models.DateTimeField(null=True) + completed = models.BooleanField(default=False) + parent_task = models.ForeignKey("self", on_delete=models.CASCADE, null=True) def __str__(self) -> str: return f"{self.title} ({self.recurrence_rule})" +class RecurrencePattern(models.Model): + """ + :param recurrence_task: The recurrence task that the pattern belongs to. + :param recurring_type: The type of recurrence. + :param max_occurrences: The maximum number of occurrences. + :param day_of_week: The day of the week that event will occure. + :param week_of_month: The week of the month that event will occure. + :param day_of_month: The day of the month that event will occure. + :param month_of_year: The month of the year that event will occure. + """ + class RecurringType(models.IntegerChoices): + DAILY = 0, 'Daily' + WEEKLY = 1, 'Weekly' + MONTHLY = 2, 'Monthly' + YEARLY = 3, 'Yearly' + + class DayOfWeek(models.IntegerChoices): + MONDAY = 0, 'Monday' + TUESDAY = 1, 'Tuesday' + WEDNESDAY = 2, 'Wednesday' + THURSDAY = 3, 'Thursday' + FRIDAY = 4, 'Friday' + SATURDAY = 5, 'Saturday' + SUNDAY = 6, 'Sunday' + + class WeekOfMonth(models.IntegerChoices): + FIRST = 1, 'First' + SECOND = 2, 'Second' + THIRD = 3, 'Third' + FOURTH = 4, 'Fourth' + LAST = 5, 'Last' + + class MonthOfYear(models.IntegerChoices): + JANUARY = 1, 'January' + FEBRUARY = 2, 'February' + MARCH = 3, 'March' + APRIL = 4, 'April' + MAY = 5, 'May' + JUNE = 6, 'June' + JULY = 7, 'July' + AUGUST = 8, 'August' + SEPTEMBER = 9, 'September' + OCTOBER = 10, 'October' + NOVEMBER = 11, 'November' + DECEMBER = 12, 'December' + + recurrence_task = models.ForeignKey(RecurrenceTask, on_delete=models.CASCADE) + recurring_type = models.IntegerField(choices=RecurringType.choices) + max_occurrences = models.IntegerField(default=0) + day_of_week = models.IntegerField(choices=DayOfWeek.choices) + week_of_month = models.IntegerField(choices=WeekOfMonth.choices) + day_of_month = models.IntegerField(default=0) + month_of_year = models.IntegerField(choices=MonthOfYear.choices) + + class Habit(Task): + """ + Represent a Habit task with streaks. + + :param streak: The streak of the habit. + :param current_count: The current count of the habit. + """ streak = models.IntegerField(default=0) + current_count = models.IntegerField(default=0) def __str__(self) -> str: return f"{self.title} ({self.streak})" @@ -90,67 +183,4 @@ class Subtask(models.Model): """ parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE) description = models.TextField() - completed = models.BooleanField(default=False) - - -class UserNotification(models.Model): - """ - Represents a user notification. - - :param type: The type of the notification (e.g., 'NEW_CHAT_MESSAGE'). - :param data: JSON data associated with the notification. - :param seen: A boolean field indicating whether the notification has been seen. - """ - NOTIFICATION_TYPES = ( - ('LEVEL_UP', 'Level Up'), - ('DEATH', 'Death'), - ) - - type = models.CharField(max_length=255, choices=[type for type in NOTIFICATION_TYPES]) - data = models.JSONField(default=dict) - seen = models.BooleanField(default=False) - - @staticmethod - def clean_notification(notifications): - """ - Cleanup function for removing corrupt notification data: - - Removes notifications with null or missing id or type. - """ - if not notifications: - return notifications - - filtered_notifications = [] - - for notification in notifications: - if notification.id is None or notification.type is None: - continue - - return filtered_notifications - - -class Transaction(models.Model): - """ - Represents a transaction involving currencies in the system. - - :param currency: The type of currency used in the transaction - :param transactionType: The type of the transaction - :param description: Additional text. - :param amount: The transaction amount. - :param user: The user involved in the transaction. - """ - CURRENCIES = (('gold', 'Gold'),) - TRANSACTION_TYPES = ( - ('buy_gold', 'Buy Gold'), - ('spend', 'Spend'), - ('debug', 'Debug'), - ('force_update_gold', 'Force Update Gold'), - ) - - currency = models.CharField(max_length=12, choices=CURRENCIES) - transaction_type = models.CharField(max_length=24, choices=TRANSACTION_TYPES) - description = models.TextField(blank=True) - amount = models.FloatField(default=0) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - - def __str__(self): - return f"Transaction ({self.id})" \ No newline at end of file + completed = models.BooleanField(default=False) \ No newline at end of file diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index 408cb55..a48330e 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -1,5 +1,4 @@ from rest_framework import serializers -from django.utils.dateparse import parse_datetime from .models import Todo, RecurrenceTask diff --git a/backend/tasks/signals.py b/backend/tasks/signals.py index af17e57..832be41 100644 --- a/backend/tasks/signals.py +++ b/backend/tasks/signals.py @@ -1,7 +1,8 @@ -from django.db.models.signals import pre_save +from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from django.utils import timezone +from boards.models import ListBoard from tasks.models import Todo @@ -22,4 +23,17 @@ def update_priority(sender, instance, **kwargs): elif time_until_due <= urgency_threshold and instance.importance < importance_threshold: instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT else: - instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT \ No newline at end of file + instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT + + +@receiver(post_save, sender=Todo) +def assign_todo_to_listboard(sender, instance, created, **kwargs): + if created: + user_board = instance.user.board_set.first() + + if user_board: + first_list_board = user_board.listboard_set.order_by('position').first() + + if first_list_board: + instance.list_board = first_list_board + instance.save() \ No newline at end of file diff --git a/backend/users/migrations/0006_remove_userstats_endurance_and_more.py b/backend/users/migrations/0006_remove_userstats_endurance_and_more.py new file mode 100644 index 0000000..a2bf209 --- /dev/null +++ b/backend/users/migrations/0006_remove_userstats_endurance_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.6 on 2023-11-19 20:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_alter_userstats_endurance_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='userstats', + name='endurance', + ), + migrations.RemoveField( + model_name='userstats', + name='intelligence', + ), + migrations.RemoveField( + model_name='userstats', + name='luck', + ), + migrations.RemoveField( + model_name='userstats', + name='perception', + ), + migrations.RemoveField( + model_name='userstats', + name='strength', + ), + migrations.AddField( + model_name='customuser', + name='last_name', + field=models.CharField(blank=True, max_length=150), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index c2eb9fd..7a765a6 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -5,16 +5,18 @@ from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin -from django.core.validators import MinValueValidator, MaxValueValidator from .managers import CustomAccountManager class CustomUser(AbstractBaseUser, PermissionsMixin): - # User fields + """ + User model where email is the unique identifier for authentication. + """ email = models.EmailField(_('email address'), unique=True) username = models.CharField(max_length=150, unique=True) first_name = models.CharField(max_length=150, blank=True) + last_name = models.CharField(max_length=150, blank=True) start_date = models.DateTimeField(default=timezone.now) about = models.TextField(_('about'), max_length=500, blank=True) profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png') @@ -35,7 +37,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # String representation of the user return self.username - def random_luck(): return random.randint(1, 50) @@ -51,17 +52,6 @@ class UserStats(models.Model): health = models.IntegerField(default=100) gold = models.FloatField(default=0.0) experience = models.FloatField(default=0) - strength = models.IntegerField(default=1, - validators=[MinValueValidator(1), - MaxValueValidator(100)]) - intelligence = models.IntegerField(default=1, validators=[MinValueValidator(1), - MaxValueValidator(100)]) - endurance = models.IntegerField(default=1, validators=[MinValueValidator(1), - MaxValueValidator(100)]) - perception = models.IntegerField(default=1, validators=[MinValueValidator(1), - MaxValueValidator(100)]) - luck = models.IntegerField(default=random_luck, validators=[MinValueValidator(1), - MaxValueValidator(50)],) @property def level(self): diff --git a/frontend/index.html b/frontend/index.html index e04fea6..ced3ebb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,5 +9,6 @@
+ diff --git a/frontend/package.json b/frontend/package.json index b610c78..f87c2ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,9 @@ "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-brands-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", "@fullcalendar/core": "^6.1.9", "@fullcalendar/daygrid": "^6.1.9", "@fullcalendar/interaction": "^6.1.9", @@ -38,7 +41,9 @@ "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "react-tsparticles": "^2.12.2", + "tsparticles": "^2.12.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.10", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index dcc1b43..5dd9b95 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,15 @@ dependencies: '@emotion/styled': specifier: ^11.11.0 version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@fortawesome/fontawesome-svg-core': + specifier: ^6.4.2 + version: 6.4.2 + '@fortawesome/free-brands-svg-icons': + specifier: ^6.4.2 + version: 6.4.2 + '@fortawesome/react-fontawesome': + specifier: ^0.2.0 + version: 0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@18.2.0) '@fullcalendar/core': specifier: ^6.1.9 version: 6.1.9 @@ -37,13 +46,13 @@ dependencies: version: 6.1.9(@fullcalendar/core@6.1.9) '@mui/icons-material': specifier: ^5.14.16 - version: 5.14.16(@mui/material@5.14.17)(@types/react@18.2.37)(react@18.2.0) + version: 5.14.18(@mui/material@5.14.18)(@types/react@18.2.37)(react@18.2.0) '@mui/material': specifier: ^5.14.17 - version: 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + version: 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@mui/system': specifier: ^5.14.17 - version: 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + version: 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) '@react-oauth/google': specifier: ^0.11.1 version: 0.11.1(react-dom@18.2.0)(react@18.2.0) @@ -55,7 +64,7 @@ dependencies: version: 23.1.36 axios: specifier: ^1.6.1 - version: 1.6.1 + version: 1.6.2 bootstrap: specifier: ^5.3.2 version: 5.3.2(@popperjs/core@2.11.8) @@ -64,7 +73,7 @@ dependencies: version: 16.3.1 framer-motion: specifier: ^10.16.4 - version: 10.16.4(react-dom@18.2.0)(react@18.2.0) + version: 10.16.5(react-dom@18.2.0)(react@18.2.0) gapi-script: specifier: ^1.2.0 version: 1.2.0 @@ -88,10 +97,16 @@ dependencies: version: 18.2.0(react@18.2.0) react-icons: specifier: ^4.11.0 - version: 4.11.0(react@18.2.0) + version: 4.12.0(react@18.2.0) react-router-dom: specifier: ^6.18.0 - version: 6.18.0(react-dom@18.2.0)(react@18.2.0) + version: 6.19.0(react-dom@18.2.0)(react@18.2.0) + react-tsparticles: + specifier: ^2.12.2 + version: 2.12.2(react@18.2.0) + tsparticles: + specifier: ^2.12.0 + version: 2.12.0 devDependencies: '@tailwindcss/typography': @@ -105,7 +120,7 @@ devDependencies: version: 18.2.15 '@vitejs/plugin-react': specifier: ^4.1.1 - version: 4.1.1(vite@4.5.0) + version: 4.2.0(vite@4.5.0) autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.31) @@ -114,16 +129,16 @@ devDependencies: version: 3.9.4 eslint: specifier: ^8.53.0 - version: 8.53.0 + version: 8.54.0 eslint-plugin-react: specifier: ^7.33.2 - version: 7.33.2(eslint@8.53.0) + version: 7.33.2(eslint@8.54.0) eslint-plugin-react-hooks: specifier: ^4.6.0 - version: 4.6.0(eslint@8.53.0) + version: 4.6.0(eslint@8.54.0) eslint-plugin-react-refresh: specifier: ^0.4.4 - version: 0.4.4(eslint@8.53.0) + version: 0.4.4(eslint@8.54.0) postcss: specifier: ^8.4.31 version: 8.4.31 @@ -742,13 +757,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.54.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.53.0 + eslint: 8.54.0 eslint-visitor-keys: 3.4.3 dev: true @@ -765,7 +780,7 @@ packages: debug: 4.3.4 espree: 9.6.1 globals: 13.23.0 - ignore: 5.2.4 + ignore: 5.3.0 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -774,8 +789,8 @@ packages: - supports-color dev: true - /@eslint/js@8.53.0: - resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} + /@eslint/js@8.54.0: + resolution: {integrity: sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -807,6 +822,39 @@ packages: resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} dev: false + /@fortawesome/fontawesome-common-types@6.4.2: + resolution: {integrity: sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + + /@fortawesome/fontawesome-svg-core@6.4.2: + resolution: {integrity: sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.2 + dev: false + + /@fortawesome/free-brands-svg-icons@6.4.2: + resolution: {integrity: sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.2 + dev: false + + /@fortawesome/react-fontawesome@0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@18.2.0): + resolution: {integrity: sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 + react: '>=16.3' + dependencies: + '@fortawesome/fontawesome-svg-core': 6.4.2 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@fullcalendar/core@6.1.9: resolution: {integrity: sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ==} dependencies: @@ -900,8 +948,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@mui/base@5.0.0-beta.23(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==} + /@mui/base@5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -913,8 +961,8 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) - '@mui/types': 7.2.8(@types/react@18.2.37) - '@mui/utils': 5.14.17(@types/react@18.2.37)(react@18.2.0) + '@mui/types': 7.2.9(@types/react@18.2.37) + '@mui/utils': 5.14.18(@types/react@18.2.37)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.37 clsx: 2.0.0 @@ -923,12 +971,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.14.17: - resolution: {integrity: sha512-eE0uxrpJAEL2ZXkeGLKg8HQDafsiXY+6eNpP4lcv3yIjFfGbU6Hj9/P7Adt8jpU+6JIhmxvILGj2r27pX+zdrQ==} + /@mui/core-downloads-tracker@5.14.18: + resolution: {integrity: sha512-yFpF35fEVDV81nVktu0BE9qn2dD/chs7PsQhlyaV3EnTeZi9RZBuvoEfRym1/jmhJ2tcfeWXiRuHG942mQXJJQ==} dev: false - /@mui/icons-material@5.14.16(@mui/material@5.14.17)(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-wmOgslMEGvbHZjFLru8uH5E+pif/ciXAvKNw16q6joK6EWVWU5rDYWFknDaZhCvz8ZE/K8ZnJQ+lMG6GgHzXbg==} + /@mui/icons-material@5.14.18(@mui/material@5.14.18)(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-o2z49R1G4SdBaxZjbMmkn+2OdT1bKymLvAYaB6pH59obM1CYv/0vAVm6zO31IqhwtYwXv6A7sLIwCGYTaVkcdg==} engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 @@ -939,13 +987,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.2 - '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.37 react: 18.2.0 dev: false - /@mui/material@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+y0VeOLWfEA4Z98We/UH6KCo8+f2HLZDK45FY+sJf8kSojLy3VntadKtC/u0itqnXXb1Pr4wKB2tSIBW02zY4Q==} + /@mui/material@5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -964,11 +1012,11 @@ packages: '@babel/runtime': 7.23.2 '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) - '@mui/base': 5.0.0-beta.23(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.14.17 - '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) - '@mui/types': 7.2.8(@types/react@18.2.37) - '@mui/utils': 5.14.17(@types/react@18.2.37)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.18 + '@mui/system': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0) + '@mui/types': 7.2.9(@types/react@18.2.37) + '@mui/utils': 5.14.18(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 '@types/react-transition-group': 4.4.9 clsx: 2.0.0 @@ -980,8 +1028,8 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.14.17(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-u4zxsCm9xmQrlhVPug+Ccrtsjv7o2+rehvrgHoh0siSguvVgVQq5O3Hh10+tp/KWQo2JR4/nCEwquSXgITS1+g==} + /@mui/private-theming@5.14.18(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-WSgjqRlzfHU+2Rou3HlR2Gqfr4rZRsvFgataYO3qQ0/m6gShJN+lhVEvwEiJ9QYyVzMDvNpXZAcqp8Y2Vl+PAw==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -991,14 +1039,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.2 - '@mui/utils': 5.14.17(@types/react@18.2.37)(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-AqpVjBEA7wnBvKPW168bNlqB6EN7HxTjLOY7oi275AzD/b1C7V0wqELy6NWoJb2yya5sRf7ENf4iNi3+T5cOgw==} + /@mui/styled-engine@5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-pW8bpmF9uCB5FV2IPk6mfbQCjPI5vGI09NOLhtGXPeph/4xIfC3JdIX0TILU0WcTs3aFQqo6s2+1SFgIB9rCXA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1019,8 +1067,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-Ccz3XlbCqka6DnbHfpL3o3TfOeWQPR+ewvNAgm8gnS9M0yVMmzzmY6z0w/C1eebb+7ZP7IoLUj9vojg/GBaTPg==} + /@mui/system@5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-hSQQdb3KF72X4EN2hMEiv8EYJZSflfdd1TRaGPoR7CIAG347OxCslpBUwWngYobaxgKvq6xTrlIl+diaactVww==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1038,10 +1086,10 @@ packages: '@babel/runtime': 7.23.2 '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) - '@mui/private-theming': 5.14.17(@types/react@18.2.37)(react@18.2.0) - '@mui/styled-engine': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) - '@mui/types': 7.2.8(@types/react@18.2.37) - '@mui/utils': 5.14.17(@types/react@18.2.37)(react@18.2.0) + '@mui/private-theming': 5.14.18(@types/react@18.2.37)(react@18.2.0) + '@mui/styled-engine': 5.14.18(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.9(@types/react@18.2.37) + '@mui/utils': 5.14.18(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 clsx: 2.0.0 csstype: 3.1.2 @@ -1049,8 +1097,8 @@ packages: react: 18.2.0 dev: false - /@mui/types@7.2.8(@types/react@18.2.37): - resolution: {integrity: sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==} + /@mui/types@7.2.9(@types/react@18.2.37): + resolution: {integrity: sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -1060,8 +1108,8 @@ packages: '@types/react': 18.2.37 dev: false - /@mui/utils@5.14.17(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==} + /@mui/utils@5.14.18(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1123,8 +1171,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@remix-run/router@1.11.0: - resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==} + /@remix-run/router@1.12.0: + resolution: {integrity: sha512-2hXv036Bux90e1GXTWSMfNzfDDK8LA8JYEWfyHxzvwdp6GyoWEovKc9cotb3KCKmkdwsIBuFGX7ScTWyiHv7Eg==} engines: {node: '>=14.0.0'} dev: false @@ -1370,11 +1418,11 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react@4.1.1(vite@4.5.0): - resolution: {integrity: sha512-Jie2HERK+uh27e+ORXXwEP5h0Y2lS9T2PRGbfebiHGlwzDO0dEnd2aNtOR/qjBlPb1YgxwAONeblL1xqLikLag==} + /@vitejs/plugin-react@4.2.0(vite@4.5.0): + resolution: {integrity: sha512-+MHTH/e6H12kRp5HUkzOGqPMksezRMmW+TNzlh/QXfI8rRf6l2Z2yH/v12no1UvTwhZgEDMuQ7g7rrfMseU6FQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 + vite: ^4.2.0 || ^5.0.0 dependencies: '@babel/core': 7.23.3 '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.3) @@ -1530,7 +1578,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.22.1 - caniuse-lite: 1.0.30001561 + caniuse-lite: 1.0.30001563 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -1543,8 +1591,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /axios@1.6.1: - resolution: {integrity: sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==} + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: follow-redirects: 1.15.3 form-data: 4.0.0 @@ -1598,8 +1646,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001561 - electron-to-chromium: 1.4.581 + caniuse-lite: 1.0.30001563 + electron-to-chromium: 1.4.588 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true @@ -1621,8 +1669,8 @@ packages: engines: {node: '>= 6'} dev: true - /caniuse-lite@1.0.30001561: - resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} + /caniuse-lite@1.0.30001563: + resolution: {integrity: sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw==} dev: true /chalk@2.4.2: @@ -1849,8 +1897,8 @@ packages: engines: {node: '>=12'} dev: false - /electron-to-chromium@1.4.581: - resolution: {integrity: sha512-6uhqWBIapTJUxgPTCHH9sqdbxIMPt7oXl0VcAL1kOtlU6aECdcMncCrX5Z7sHQ/invtrC9jUQUef7+HhO8vVFw==} + /electron-to-chromium@1.4.588: + resolution: {integrity: sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==} dev: true /error-ex@1.3.2: @@ -1990,24 +2038,24 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - /eslint-plugin-react-hooks@4.6.0(eslint@8.53.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.54.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.53.0 + eslint: 8.54.0 dev: true - /eslint-plugin-react-refresh@0.4.4(eslint@8.53.0): + /eslint-plugin-react-refresh@0.4.4(eslint@8.54.0): resolution: {integrity: sha512-eD83+65e8YPVg6603Om2iCIwcQJf/y7++MWm4tACtEswFLYMwxwVWAfwN+e19f5Ad/FOyyNg9Dfi5lXhH3Y3rA==} peerDependencies: eslint: '>=7' dependencies: - eslint: 8.53.0 + eslint: 8.54.0 dev: true - /eslint-plugin-react@7.33.2(eslint@8.53.0): + /eslint-plugin-react@7.33.2(eslint@8.54.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} peerDependencies: @@ -2018,7 +2066,7 @@ packages: array.prototype.tosorted: 1.1.2 doctrine: 2.1.0 es-iterator-helpers: 1.0.15 - eslint: 8.53.0 + eslint: 8.54.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.5 minimatch: 3.1.2 @@ -2045,15 +2093,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.53.0: - resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} + /eslint@8.54.0: + resolution: {integrity: sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 2.1.3 - '@eslint/js': 8.53.0 + '@eslint/js': 8.54.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -2075,7 +2123,7 @@ packages: glob-parent: 6.0.2 globals: 13.23.0 graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -2162,7 +2210,7 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flat-cache: 3.1.1 + flat-cache: 3.2.0 dev: true /fill-range@7.0.1: @@ -2184,9 +2232,9 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache@3.1.1: - resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} - engines: {node: '>=12.0.0'} + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} dependencies: flatted: 3.2.9 keyv: 4.5.4 @@ -2226,8 +2274,8 @@ packages: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true - /framer-motion@10.16.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==} + /framer-motion@10.16.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GEzVjOYP2MIpV9bT/GbhcsBNoImG3/2X3O/xVNWmktkv9MdJ7P/44zELm/7Fjb+O3v39SmKFnoDQB32giThzpg==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -2419,8 +2467,8 @@ packages: react-is: 16.13.1 dev: false - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} dev: true @@ -3278,6 +3326,8 @@ packages: /react-icons@4.11.0(react@18.2.0): resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==} + /react-icons@4.12.0(react@18.2.0): + resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} peerDependencies: react: '*' dependencies: @@ -3326,26 +3376,26 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-router-dom@6.18.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==} + /react-router-dom@6.19.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N6dWlcgL2w0U5HZUUqU2wlmOrSb3ighJmtQ438SWbhB1yuLTXQ8yyTBMK3BSvVjp7gBtKurT554nCtMOgxCZmQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.11.0 + '@remix-run/router': 1.12.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.18.0(react@18.2.0) + react-router: 6.19.0(react@18.2.0) dev: false - /react-router@6.18.0(react@18.2.0): - resolution: {integrity: sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==} + /react-router@6.19.0(react@18.2.0): + resolution: {integrity: sha512-0W63PKCZ7+OuQd7Tm+RbkI8kCLmn4GPjDbX61tWljPxWgqTKlEpeQUwPkT1DRjYhF8KSihK0hQpmhU4uxVMcdw==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.11.0 + '@remix-run/router': 1.12.0 react: 18.2.0 dev: false @@ -3388,6 +3438,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-tsparticles@2.12.2(react@18.2.0): + resolution: {integrity: sha512-/nrEbyL8UROXKIMXe+f+LZN2ckvkwV2Qa+GGe/H26oEIc+wq/ybSG9REDwQiSt2OaDQGu0MwmA4BKmkL6wAWcA==} + requiresBuild: true + peerDependencies: + react: '>=16' + dependencies: + react: 18.2.0 + tsparticles-engine: 2.12.0 + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3725,6 +3785,310 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: false + /tsparticles-basic@2.12.0: + resolution: {integrity: sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g==} + dependencies: + tsparticles-engine: 2.12.0 + tsparticles-move-base: 2.12.0 + tsparticles-shape-circle: 2.12.0 + tsparticles-updater-color: 2.12.0 + tsparticles-updater-opacity: 2.12.0 + tsparticles-updater-out-modes: 2.12.0 + tsparticles-updater-size: 2.12.0 + dev: false + + /tsparticles-engine@2.12.0: + resolution: {integrity: sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ==} + requiresBuild: true + dev: false + + /tsparticles-interaction-external-attract@2.12.0: + resolution: {integrity: sha512-0roC6D1QkFqMVomcMlTaBrNVjVOpyNzxIUsjMfshk2wUZDAvTNTuWQdUpmsLS4EeSTDN3rzlGNnIuuUQqyBU5w==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-bounce@2.12.0: + resolution: {integrity: sha512-MMcqKLnQMJ30hubORtdq+4QMldQ3+gJu0bBYsQr9BsThsh8/V0xHc1iokZobqHYVP5tV77mbFBD8Z7iSCf0TMQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-bubble@2.12.0: + resolution: {integrity: sha512-5kImCSCZlLNccXOHPIi2Yn+rQWTX3sEa/xCHwXW19uHxtILVJlnAweayc8+Zgmb7mo0DscBtWVFXHPxrVPFDUA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-connect@2.12.0: + resolution: {integrity: sha512-ymzmFPXz6AaA1LAOL5Ihuy7YSQEW8MzuSJzbd0ES13U8XjiU3HlFqlH6WGT1KvXNw6WYoqrZt0T3fKxBW3/C3A==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-grab@2.12.0: + resolution: {integrity: sha512-iQF/A947hSfDNqAjr49PRjyQaeRkYgTYpfNmAf+EfME8RsbapeP/BSyF6mTy0UAFC0hK2A2Hwgw72eT78yhXeQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-pause@2.12.0: + resolution: {integrity: sha512-4SUikNpsFROHnRqniL+uX2E388YTtfRWqqqZxRhY0BrijH4z04Aii3YqaGhJxfrwDKkTQlIoM2GbFT552QZWjw==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-push@2.12.0: + resolution: {integrity: sha512-kqs3V0dgDKgMoeqbdg+cKH2F+DTrvfCMrPF1MCCUpBCqBiH+TRQpJNNC86EZYHfNUeeLuIM3ttWwIkk2hllR/Q==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-remove@2.12.0: + resolution: {integrity: sha512-2eNIrv4m1WB2VfSVj46V2L/J9hNEZnMgFc+A+qmy66C8KzDN1G8aJUAf1inW8JVc0lmo5+WKhzex4X0ZSMghBg==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-repulse@2.12.0: + resolution: {integrity: sha512-rSzdnmgljeBCj5FPp4AtGxOG9TmTsK3AjQW0vlyd1aG2O5kSqFjR+FuT7rfdSk9LEJGH5SjPFE6cwbuy51uEWA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-slow@2.12.0: + resolution: {integrity: sha512-2IKdMC3om7DttqyroMtO//xNdF0NvJL/Lx7LDo08VpfTgJJozxU+JAUT8XVT7urxhaDzbxSSIROc79epESROtA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-external-trail@2.12.0: + resolution: {integrity: sha512-LKSapU5sPTaZqYx+y5VJClj0prlV7bswplSFQaIW1raXkvsk45qir2AVcpP5JUhZSFSG+SwsHr+qCgXhNeN1KA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-particles-attract@2.12.0: + resolution: {integrity: sha512-Hl8qwuwF9aLq3FOkAW+Zomu7Gb8IKs6Y3tFQUQScDmrrSCaeRt2EGklAiwgxwgntmqzL7hbMWNx06CHHcUQKdQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-particles-collisions@2.12.0: + resolution: {integrity: sha512-Se9nPWlyPxdsnHgR6ap4YUImAu3W5MeGKJaQMiQpm1vW8lSMOUejI1n1ioIaQth9weKGKnD9rvcNn76sFlzGBA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-interaction-particles-links@2.12.0: + resolution: {integrity: sha512-e7I8gRs4rmKfcsHONXMkJnymRWpxHmeaJIo4g2NaDRjIgeb2AcJSWKWZvrsoLnm7zvaf/cMQlbN6vQwCixYq3A==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-move-base@2.12.0: + resolution: {integrity: sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-move-parallax@2.12.0: + resolution: {integrity: sha512-58CYXaX8Ih5rNtYhpnH0YwU4Ks7gVZMREGUJtmjhuYN+OFr9FVdF3oDIJ9N6gY5a5AnAKz8f5j5qpucoPRcYrQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-particles.js@2.12.0: + resolution: {integrity: sha512-LyOuvYdhbUScmA4iDgV3LxA0HzY1DnOwQUy3NrPYO393S2YwdDjdwMod6Btq7EBUjg9FVIh+sZRizgV5elV2dg==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-plugin-absorbers@2.12.0: + resolution: {integrity: sha512-2CkPreaXHrE5VzFlxUKLeRB5t66ff+3jwLJoDFgQcp+R4HOEITo0bBZv2DagGP0QZdYN4grpnQzRBVdB4d1rWA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-plugin-easing-quad@2.12.0: + resolution: {integrity: sha512-2mNqez5pydDewMIUWaUhY5cNQ80IUOYiujwG6qx9spTq1D6EEPLbRNAEL8/ecPdn2j1Um3iWSx6lo340rPkv4Q==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-plugin-emitters@2.12.0: + resolution: {integrity: sha512-fbskYnaXWXivBh9KFReVCfqHdhbNQSK2T+fq2qcGEWpwtDdgujcaS1k2Q/xjZnWNMfVesik4IrqspcL51gNdSA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-circle@2.12.0: + resolution: {integrity: sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-image@2.12.0: + resolution: {integrity: sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-line@2.12.0: + resolution: {integrity: sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-polygon@2.12.0: + resolution: {integrity: sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-square@2.12.0: + resolution: {integrity: sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-star@2.12.0: + resolution: {integrity: sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-shape-text@2.12.0: + resolution: {integrity: sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-slim@2.12.0: + resolution: {integrity: sha512-27w9aGAAAPKHvP4LHzWFpyqu7wKyulayyaZ/L6Tuuejy4KP4BBEB4rY5GG91yvAPsLtr6rwWAn3yS+uxnBDpkA==} + dependencies: + tsparticles-basic: 2.12.0 + tsparticles-engine: 2.12.0 + tsparticles-interaction-external-attract: 2.12.0 + tsparticles-interaction-external-bounce: 2.12.0 + tsparticles-interaction-external-bubble: 2.12.0 + tsparticles-interaction-external-connect: 2.12.0 + tsparticles-interaction-external-grab: 2.12.0 + tsparticles-interaction-external-pause: 2.12.0 + tsparticles-interaction-external-push: 2.12.0 + tsparticles-interaction-external-remove: 2.12.0 + tsparticles-interaction-external-repulse: 2.12.0 + tsparticles-interaction-external-slow: 2.12.0 + tsparticles-interaction-particles-attract: 2.12.0 + tsparticles-interaction-particles-collisions: 2.12.0 + tsparticles-interaction-particles-links: 2.12.0 + tsparticles-move-base: 2.12.0 + tsparticles-move-parallax: 2.12.0 + tsparticles-particles.js: 2.12.0 + tsparticles-plugin-easing-quad: 2.12.0 + tsparticles-shape-circle: 2.12.0 + tsparticles-shape-image: 2.12.0 + tsparticles-shape-line: 2.12.0 + tsparticles-shape-polygon: 2.12.0 + tsparticles-shape-square: 2.12.0 + tsparticles-shape-star: 2.12.0 + tsparticles-shape-text: 2.12.0 + tsparticles-updater-color: 2.12.0 + tsparticles-updater-life: 2.12.0 + tsparticles-updater-opacity: 2.12.0 + tsparticles-updater-out-modes: 2.12.0 + tsparticles-updater-rotate: 2.12.0 + tsparticles-updater-size: 2.12.0 + tsparticles-updater-stroke-color: 2.12.0 + dev: false + + /tsparticles-updater-color@2.12.0: + resolution: {integrity: sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-destroy@2.12.0: + resolution: {integrity: sha512-6NN3dJhxACvzbIGL4dADbYQSZJmdHfwjujj1uvnxdMbb2x8C/AZzGxiN33smo4jkrZ5VLEWZWCJPJ8aOKjQ2Sg==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-life@2.12.0: + resolution: {integrity: sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-opacity@2.12.0: + resolution: {integrity: sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-out-modes@2.12.0: + resolution: {integrity: sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-roll@2.12.0: + resolution: {integrity: sha512-dxoxY5jP4C9x15BxlUv5/Q8OjUPBiE09ToXRyBxea9aEJ7/iMw6odvi1HuT0H1vTIfV7o1MYawjeCbMycvODKQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-rotate@2.12.0: + resolution: {integrity: sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-size@2.12.0: + resolution: {integrity: sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-stroke-color@2.12.0: + resolution: {integrity: sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-tilt@2.12.0: + resolution: {integrity: sha512-HDEFLXazE+Zw+kkKKAiv0Fs9D9sRP61DoCR6jZ36ipea6OBgY7V1Tifz2TSR1zoQkk57ER9+EOQbkSQO+YIPGQ==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-twinkle@2.12.0: + resolution: {integrity: sha512-JhK/DO4kTx7IFwMBP2EQY9hBaVVvFnGBvX21SQWcjkymmN1hZ+NdcgUtR9jr4jUiiSNdSl7INaBuGloVjWvOgA==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles-updater-wobble@2.12.0: + resolution: {integrity: sha512-85FIRl95ipD3jfIsQdDzcUC5PRMWIrCYqBq69nIy9P8rsNzygn+JK2n+P1VQZowWsZvk0mYjqb9OVQB21Lhf6Q==} + dependencies: + tsparticles-engine: 2.12.0 + dev: false + + /tsparticles@2.12.0: + resolution: {integrity: sha512-aw77llkaEhcKYUHuRlggA6SB1Dpa814/nrStp9USGiDo5QwE1Ckq30QAgdXU6GRvnblUFsiO750ZuLQs5Y0tVw==} + dependencies: + tsparticles-engine: 2.12.0 + tsparticles-interaction-external-trail: 2.12.0 + tsparticles-plugin-absorbers: 2.12.0 + tsparticles-plugin-emitters: 2.12.0 + tsparticles-slim: 2.12.0 + tsparticles-updater-destroy: 2.12.0 + tsparticles-updater-roll: 2.12.0 + tsparticles-updater-tilt: 2.12.0 + tsparticles-updater-twinkle: 2.12.0 + tsparticles-updater-wobble: 2.12.0 + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} diff --git a/frontend/src/App.css b/frontend/src/App.css index 48931c9..eaac616 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -43,4 +43,4 @@ to { transform: rotate(360deg); } -} +} \ No newline at end of file diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx index d72491a..defeaea 100644 --- a/frontend/src/PrivateRoute.jsx +++ b/frontend/src/PrivateRoute.jsx @@ -8,4 +8,4 @@ const PrivateRoute = () => { return auth ? : ; }; -export default PrivateRoute; +export default PrivateRoute; \ No newline at end of file diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx index d1eedf9..a48c075 100644 --- a/frontend/src/components/authentication/LoginPage.jsx +++ b/frontend/src/components/authentication/LoginPage.jsx @@ -1,15 +1,17 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useGoogleLogin } from "@react-oauth/google"; - +import { useCallback } from "react"; +import Particles from "react-tsparticles"; +import { loadFull } from "tsparticles"; import refreshAccessToken from "./refreshAcesstoken"; import axiosapi from "../../api/AuthenticationApi"; +import { useAuth } from "../../hooks/authentication/IsAuthenticated"; +import { FcGoogle } from "react-icons/fc"; -import { useAuth } from "../../hooks/authentication/IsAuthenticated"; function LoginPage() { const Navigate = useNavigate(); - const { isAuthenticated, setIsAuthenticated } = useAuth(); useEffect(() => { @@ -21,15 +23,14 @@ function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const handleEmailChange = event => { + const handleEmailChange = (event) => { setEmail(event.target.value); }; - const handlePasswordChange = event => { + const handlePasswordChange = (event) => { setPassword(event.target.value); }; - - const handleSubmit = event => { + const handleSubmit = (event) => { event.preventDefault(); // Send a POST request to the authentication API @@ -38,15 +39,16 @@ function LoginPage() { email: email, password: password, }) - .then(res => { + .then((res) => { // On successful login, store tokens and set the authorization header localStorage.setItem("access_token", res.data.access); localStorage.setItem("refresh_token", res.data.refresh); - axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access; + axiosapi.axiosInstance.defaults.headers["Authorization"] = + "Bearer " + res.data.access; setIsAuthenticated(true); Navigate("/"); }) - .catch(err => { + .catch((err) => { console.log("Login failed"); console.log(err); setIsAuthenticated(false); @@ -56,7 +58,7 @@ function LoginPage() { const googleLoginImplicit = useGoogleLogin({ flow: "auth-code", redirect_uri: "postmessage", - onSuccess: async response => { + onSuccess: async (response) => { try { const loginResponse = await axiosapi.googleLogin(response.code); if (loginResponse && loginResponse.data) { @@ -72,17 +74,104 @@ function LoginPage() { setIsAuthenticated(false); } }, - onError: errorResponse => console.log(errorResponse), + onError: (errorResponse) => console.log(errorResponse), }); + { + /* Particles Loader*/ + } + const particlesInit = useCallback(async (engine) => { + console.log(engine); + await loadFull(engine); + }, []); + + const particlesLoaded = useCallback(async (container) => { + console.log(container); + }, []); return ( -
- {/* Left Section (Login Box) */} -
-
-

Log in to your account

+
+ {/* Particles Container */} +
+ +
+ {/* Login Box */} +
+
+

Login

{/* Email Input */} -
+
{/* Login Button */} -
OR
{/* Login with Google Button */} - {/* Forgot Password Link */}
@@ -128,20 +220,6 @@ function LoginPage() {
- - {/* Right Section (Blurred Image Background) */} -
-
- -
- Text Overlay -
-
); } diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx index 28eb1c2..50d893b 100644 --- a/frontend/src/components/authentication/SignUpPage.jsx +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -1,36 +1,29 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import axiosapi from "../../api/AuthenticationApi"; +import { useCallback } from "react"; +import Particles from "react-tsparticles"; +import { loadFull } from "tsparticles"; +import { FcGoogle } from "react-icons/fc"; +import { useGoogleLogin } from "@react-oauth/google"; -import Avatar from "@mui/material/Avatar"; -import Button from "@mui/material/Button"; -import CssBaseline from "@mui/material/CssBaseline"; -import TextField from "@mui/material/TextField"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Checkbox from "@mui/material/Checkbox"; -import Link from "@mui/material/Link"; -import Grid from "@mui/material/Grid"; -import Box from "@mui/material/Box"; -import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; -import Typography from "@mui/material/Typography"; -import Container from "@mui/material/Container"; -import { createTheme, ThemeProvider } from "@mui/material/styles"; function Copyright(props) { return ( - +
{"Copyright © "} - + TurTask - {" "} + {" "} {new Date().getFullYear()} {"."} - +
); } -const defaultTheme = createTheme(); - export default function SignUp() { const Navigate = useNavigate(); @@ -42,7 +35,7 @@ export default function SignUp() { const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = async e => { + const handleSubmit = async (e) => { e.preventDefault(); setIsSubmitting(true); setError(null); @@ -58,86 +51,194 @@ export default function SignUp() { Navigate("/login"); }; - const handleChange = e => { + const handleChange = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; + { + /* Particles Loader*/ + } + const particlesInit = useCallback(async (engine) => { + console.log(engine); + await loadFull(engine); + }, []); + + const particlesLoaded = useCallback(async (container) => { + console.log(container); + }, []); + + const googleLoginImplicit = useGoogleLogin({ + flow: "auth-code", + redirect_uri: "postmessage", + onSuccess: async (response) => { + try { + const loginResponse = await axiosapi.googleLogin(response.code); + if (loginResponse && loginResponse.data) { + const { access_token, refresh_token } = loginResponse.data; + + localStorage.setItem("access_token", access_token); + localStorage.setItem("refresh_token", refresh_token); + setIsAuthenticated(true); + Navigate("/"); + } + } catch (error) { + console.error("Error with the POST request:", error); + setIsAuthenticated(false); + } + }, + onError: (errorResponse) => console.log(errorResponse), + }); return ( - - - - - - - - - Sign up - - - - - - - - - - - - - - } - label="I want to receive inspiration, marketing promotions and updates via email." - /> - - - - - - - Already have an account? Sign in - - - - - - - - +
+ {/* Particles Container */} +
+ +
+
+
+ {/* Register Form */} +

Signup

+ {/* Email Input */} +
+ + +
+ {/* Username Input */} +
+ + +
+ {/* Password Input */} +
+ + +
+

+ + {/* Login Button */} + +
OR
+ {/* Login with Google Button */} + + {/* Already have an account? */} + + +
+
+
); } diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 2ec2914..7866bbb 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -136,6 +136,7 @@ function KanbanBoard() { ))}
+ {/* create new column */} +
+ +
+ +
+ +
+ +
+
+
+ + ); +} + +export default Signup;