mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Merge branch 'main' into feature/the-eisenhower-matrix
This commit is contained in:
commit
a9859c0789
@ -6,4 +6,6 @@
|
|||||||
|
|
||||||
TurTask is a task and project management tool that incorporates gamification elements.
|
TurTask is a task and project management tool that incorporates gamification elements.
|
||||||
|
|
||||||
[Wiki Repository](https://github.com/TurTaskProject/TurTaskWiki)
|
[Wiki Page](https://github.com/TurTaskProject/TurTaskWeb/wiki)
|
||||||
|
|
||||||
|
[Project Board](https://github.com/orgs/TurTaskProject/projects/1)
|
||||||
|
|||||||
@ -7,8 +7,8 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from tasks.utils import get_service
|
from tasks.utils import get_service
|
||||||
from tasks.models import Task
|
from tasks.models import Todo, RecurrenceTask
|
||||||
from tasks.serializers import TaskUpdateSerializer
|
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
||||||
@ -17,28 +17,34 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_time = datetime.now(tz=timezone.utc).isoformat()
|
self.current_time = datetime.now(tz=timezone.utc).isoformat()
|
||||||
self.event_fields = 'items(id,summary,description,created,updated,start,end)'
|
self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)'
|
||||||
|
|
||||||
def _validate_serializer(self, serializer):
|
def _validate_serializer(self, serializer):
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response("Task Sync Successfully", status=200)
|
return Response("Validate Successfully", status=200)
|
||||||
return Response(serializer.errors, status=400)
|
return Response(serializer.errors, status=400)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
service = get_service(request)
|
service = get_service(request)
|
||||||
events = service.events().list(calendarId='primary', fields=self.event_fields).execute()
|
events = service.events().list(calendarId='primary', fields=self.event_fields).execute()
|
||||||
for event in events.get('items', []):
|
for event in events.get('items', []):
|
||||||
|
if event.get('recurringEventId'):
|
||||||
|
continue
|
||||||
|
event['start_datetime'] = event.get('start').get('dateTime')
|
||||||
|
event['end_datetime'] = event.get('end').get('dateTime')
|
||||||
|
event.pop('start')
|
||||||
|
event.pop('end')
|
||||||
try:
|
try:
|
||||||
task = Task.objects.get(google_calendar_id=event['id'])
|
task = Todo.objects.get(google_calendar_id=event['id'])
|
||||||
serializer = TaskUpdateSerializer(instance=task, data=event)
|
serializer = TodoUpdateSerializer(instance=task, data=event)
|
||||||
return self._validate_serializer(serializer)
|
return self._validate_serializer(serializer)
|
||||||
except Task.DoesNotExist:
|
except Todo.DoesNotExist:
|
||||||
serializer = TaskUpdateSerializer(data=event, user=request.user)
|
serializer = TodoUpdateSerializer(data=event, user=request.user)
|
||||||
return self._validate_serializer(serializer)
|
return self._validate_serializer(serializer)
|
||||||
|
|
||||||
def list(self, request, days=7):
|
def list(self, request, days=7):
|
||||||
max_time = (datetime.now(tz=timezone.utc) + timedelta(days=3)).isoformat()
|
max_time = (datetime.now(tz=timezone.utc) + timedelta(days=days)).isoformat()
|
||||||
|
|
||||||
service = get_service(request)
|
service = get_service(request)
|
||||||
events = []
|
events = []
|
||||||
@ -49,11 +55,11 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
calendarId='primary',
|
calendarId='primary',
|
||||||
timeMin=self.current_time,
|
timeMin=self.current_time,
|
||||||
timeMax=max_time,
|
timeMax=max_time,
|
||||||
maxResults=20,
|
maxResults=200,
|
||||||
singleEvents=True,
|
singleEvents=True,
|
||||||
orderBy='startTime',
|
orderBy='startTime',
|
||||||
pageToken=next_page_token,
|
pageToken=next_page_token,
|
||||||
fields='items(id,summary,description,created,updated,start,end)',
|
fields='items(id,summary,description,created,recurringEventId,updated,start,end)',
|
||||||
)
|
)
|
||||||
|
|
||||||
page_results = query.execute()
|
page_results = query.execute()
|
||||||
|
|||||||
@ -4,3 +4,6 @@ from django.apps import AppConfig
|
|||||||
class TasksConfig(AppConfig):
|
class TasksConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'tasks'
|
name = 'tasks'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import tasks.signals
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-06 15:01
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('tasks', '0009_alter_task_options_task_importance_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Todo',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.TextField()),
|
||||||
|
('notes', models.TextField(default='')),
|
||||||
|
('importance', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=1)),
|
||||||
|
('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1)),
|
||||||
|
('challenge', models.BooleanField(default=False)),
|
||||||
|
('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(blank=True, max_length=255, null=True)),
|
||||||
|
('start_event', models.DateTimeField(null=True)),
|
||||||
|
('end_event', models.DateTimeField(null=True)),
|
||||||
|
('priority', models.PositiveSmallIntegerField(choices=[(1, 'Important & Urgent'), (2, 'Important & Not Urgent'), (3, 'Not Important & Urgent'), (4, 'Not Important & Not Urgent')], default=4)),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='tasks.tag')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='subtask',
|
||||||
|
name='parent_task',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.todo'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Task',
|
||||||
|
),
|
||||||
|
]
|
||||||
39
backend/tasks/migrations/0011_recurrencetask.py
Normal file
39
backend/tasks/migrations/0011_recurrencetask.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-06 16:14
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('tasks', '0010_todo_alter_subtask_parent_task_delete_task'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecurrenceTask',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.TextField()),
|
||||||
|
('notes', models.TextField(default='')),
|
||||||
|
('importance', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=1)),
|
||||||
|
('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1)),
|
||||||
|
('challenge', models.BooleanField(default=False)),
|
||||||
|
('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(blank=True, max_length=255, null=True)),
|
||||||
|
('start_event', models.DateTimeField(null=True)),
|
||||||
|
('end_event', models.DateTimeField(null=True)),
|
||||||
|
('recurrence_rule', models.TextField()),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='tasks.tag')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,9 +1,5 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
class Tag(models.Model):
|
class Tag(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -16,21 +12,16 @@ class Tag(models.Model):
|
|||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a task, such as Habit, Daily, Todo, or Reward.
|
Represents a Abstract of task, such as Habit, Daily, Todo, or Reward.
|
||||||
|
|
||||||
:param type: The type of the tasks
|
:param user: The user who owns the task.
|
||||||
:param title: Title of the task.
|
:param title: Title of the task.
|
||||||
:param notes: Optional additional notes for the task.
|
:param notes: Optional additional notes for the task.
|
||||||
:param tags: Associated tags for the task.
|
:param tags: Associated tags for the task.
|
||||||
:param completed: A boolean field indicating whether the task is completed.
|
:param completed: A boolean field indicating whether the task is completed.
|
||||||
:param exp: The experience values user will get from the task.
|
|
||||||
:param priority: The priority of the task (1, 2, .., 4), using Eisenhower Matrix Idea.
|
|
||||||
:param importance: The importance of the task (range: 1 to 5)
|
:param importance: The importance of the task (range: 1 to 5)
|
||||||
:param difficulty: The difficulty of the task (range: 1 to 5).
|
:param difficulty: The difficulty of the task (range: 1 to 5).
|
||||||
:param attribute: The attribute linked to the task
|
|
||||||
:param user: The user who owns the task.
|
|
||||||
:param challenge: Associated challenge (optional).
|
:param challenge: Associated challenge (optional).
|
||||||
:param reminders: A Many-to-Many relationship with Reminder.
|
|
||||||
:param fromSystem: A boolean field indicating if the task is from System.
|
:param fromSystem: A boolean field indicating if the task is from System.
|
||||||
:param creation_date: Creation date of the task.
|
:param creation_date: Creation date of the task.
|
||||||
:param last_update: Last updated date of the task.
|
:param last_update: Last updated date of the task.
|
||||||
@ -38,87 +29,49 @@ class Task(models.Model):
|
|||||||
:param start_event: Start event of the task.
|
:param start_event: Start event of the task.
|
||||||
:param end_event: End event(Due Date) of the task.
|
:param end_event: End event(Due Date) of the task.
|
||||||
"""
|
"""
|
||||||
TASK_TYPES = [
|
class Difficulty(models.IntegerChoices):
|
||||||
('daily', 'Daily'),
|
EASY = 1, 'Easy'
|
||||||
('habit', 'Habit'),
|
NORMAL = 2, 'Normal'
|
||||||
('todo', 'Todo'),
|
HARD = 3, 'Hard'
|
||||||
('Long Term Goal', 'Long Term Goal'),
|
VERY_HARD = 4, 'Very Hard'
|
||||||
]
|
DEVIL = 5, 'Devil'
|
||||||
|
|
||||||
DIFFICULTY_CHOICES = [
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
(1, 'Easy'),
|
|
||||||
(2, 'Normal'),
|
|
||||||
(3, 'Hard'),
|
|
||||||
(4, 'Very Hard'),
|
|
||||||
(5, 'Devil'),
|
|
||||||
]
|
|
||||||
|
|
||||||
ATTRIBUTE = [
|
|
||||||
('str', 'Strength'),
|
|
||||||
('int', 'Intelligence'),
|
|
||||||
('end', 'Endurance'),
|
|
||||||
('per', 'Perception'),
|
|
||||||
('luck', 'Luck'),
|
|
||||||
]
|
|
||||||
|
|
||||||
EISENHOWER_MATRIX = [
|
|
||||||
(1, 'Important & Urgent'),
|
|
||||||
(2, 'Important & Not Urgent'),
|
|
||||||
(3, 'Not Important & Urgent'),
|
|
||||||
(4, 'Not Important & Not Urgent'),
|
|
||||||
]
|
|
||||||
|
|
||||||
type = models.CharField(max_length=15, choices=TASK_TYPES, default='habit')
|
|
||||||
title = models.TextField()
|
title = models.TextField()
|
||||||
notes = models.TextField(default='')
|
notes = models.TextField(default='')
|
||||||
tags = models.ManyToManyField(Tag, blank=True)
|
tags = models.ManyToManyField(Tag, blank=True)
|
||||||
completed = models.BooleanField(default=False)
|
|
||||||
exp = models.FloatField(default=0)
|
|
||||||
priority = models.PositiveSmallIntegerField(choices=EISENHOWER_MATRIX, default=4)
|
|
||||||
importance = models.PositiveSmallIntegerField(choices=[(i, str(i)) for i in range(1, 6)], default=1)
|
importance = models.PositiveSmallIntegerField(choices=[(i, str(i)) for i in range(1, 6)], default=1)
|
||||||
difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, default=1)
|
difficulty = models.PositiveSmallIntegerField(choices=Difficulty.choices, default=Difficulty.EASY)
|
||||||
attribute = models.CharField(max_length=15, choices=ATTRIBUTE, default='str')
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
||||||
challenge = models.BooleanField(default=False)
|
challenge = models.BooleanField(default=False)
|
||||||
fromSystem = models.BooleanField(default=False)
|
fromSystem = models.BooleanField(default=False)
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
last_update = models.DateTimeField(auto_now=True)
|
last_update = models.DateTimeField(auto_now=True)
|
||||||
google_calendar_id = models.CharField(blank=True, null=True, max_length=255)
|
google_calendar_id = models.CharField(max_length=255, null=True, blank=True)
|
||||||
start_event = models.DateTimeField(null=True)
|
start_event = models.DateTimeField(null=True)
|
||||||
end_event = models.DateTimeField(null=True)
|
end_event = models.DateTimeField(null=True)
|
||||||
|
|
||||||
def calculate_eisenhower_matrix_category(self):
|
|
||||||
"""
|
|
||||||
Classify the task into one of the four categories in the Eisenhower Matrix.
|
|
||||||
|
|
||||||
:return: The category of the task (1, 2, 3, or 4).
|
|
||||||
"""
|
|
||||||
if self.end_event:
|
|
||||||
time_until_due = (self.end_event - datetime.now(timezone.utc)).days
|
|
||||||
else:
|
|
||||||
time_until_due = float('inf')
|
|
||||||
|
|
||||||
urgency_threshold = 3
|
|
||||||
importance_threshold = 3
|
|
||||||
|
|
||||||
if time_until_due <= urgency_threshold and self.importance >= importance_threshold:
|
|
||||||
return 1
|
|
||||||
elif time_until_due > urgency_threshold and self.importance >= importance_threshold:
|
|
||||||
return 2
|
|
||||||
elif time_until_due <= urgency_threshold and self.importance < importance_threshold:
|
|
||||||
return 3
|
|
||||||
else:
|
|
||||||
return 4
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.priority = self.calculate_eisenhower_matrix_category()
|
|
||||||
super(Task, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Task'
|
abstract = True
|
||||||
verbose_name_plural = 'Tasks'
|
|
||||||
|
|
||||||
|
|
||||||
|
class Todo(Task):
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class RecurrenceTask(Task):
|
||||||
|
recurrence_rule = models.TextField()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.title} ({self.recurrence_rule})"
|
||||||
|
|
||||||
class Subtask(models.Model):
|
class Subtask(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -127,9 +80,9 @@ class Subtask(models.Model):
|
|||||||
:param completed: A boolean field indicating whether the subtask is completed.
|
:param completed: A boolean field indicating whether the subtask is completed.
|
||||||
:param parent_task: The parent task of the subtask.
|
:param parent_task: The parent task of the subtask.
|
||||||
"""
|
"""
|
||||||
|
parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
completed = models.BooleanField(default=False)
|
completed = models.BooleanField(default=False)
|
||||||
parent_task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
class UserNotification(models.Model):
|
class UserNotification(models.Model):
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
from .models import Task
|
from .models import Todo, RecurrenceTask
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarEventSerializer(serializers.Serializer):
|
class GoogleCalendarEventSerializer(serializers.Serializer):
|
||||||
@ -10,7 +10,7 @@ class GoogleCalendarEventSerializer(serializers.Serializer):
|
|||||||
description = serializers.CharField(required=False)
|
description = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class TaskUpdateSerializer(serializers.ModelSerializer):
|
class TodoUpdateSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.CharField(source="google_calendar_id")
|
id = serializers.CharField(source="google_calendar_id")
|
||||||
summary = serializers.CharField(source="title")
|
summary = serializers.CharField(source="title")
|
||||||
description = serializers.CharField(source="notes", required=False)
|
description = serializers.CharField(source="notes", required=False)
|
||||||
@ -21,15 +21,41 @@ class TaskUpdateSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Todo
|
||||||
fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime')
|
fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user', None)
|
self.user = kwargs.pop('user', None)
|
||||||
super(TaskUpdateSerializer, self).__init__(*args, **kwargs)
|
super(TodoUpdateSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['user'] = self.user
|
validated_data['user'] = self.user
|
||||||
task = Task.objects.create(**validated_data)
|
task = Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrenceTaskUpdateSerializer(serializers.ModelSerializer):
|
||||||
|
id = serializers.CharField(source="google_calendar_id")
|
||||||
|
summary = serializers.CharField(source="title")
|
||||||
|
description = serializers.CharField(source="notes", required=False)
|
||||||
|
created = serializers.DateTimeField(source="creation_date")
|
||||||
|
updated = serializers.DateTimeField(source="last_update")
|
||||||
|
recurrence = serializers.DateTimeField(source="recurrence_rule")
|
||||||
|
start_datetime = serializers.DateTimeField(source="start_event", required=False)
|
||||||
|
end_datetime = serializers.DateTimeField(source="end_event", required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RecurrenceTask
|
||||||
|
fields = ('id', 'summary', 'description', 'created', 'updated', 'recurrence', 'start_datetime', 'end_datetime')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop('user', None)
|
||||||
|
super(RecurrenceTaskUpdateSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['user'] = self.user
|
||||||
|
task = RecurrenceTask.objects.create(**validated_data)
|
||||||
|
|
||||||
return task
|
return task
|
||||||
25
backend/tasks/signals.py
Normal file
25
backend/tasks/signals.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from tasks.models import Todo
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=Todo)
|
||||||
|
def update_priority(sender, instance, **kwargs):
|
||||||
|
if instance.end_event:
|
||||||
|
time_until_due = (instance.end_event - timezone.now()).days
|
||||||
|
else:
|
||||||
|
time_until_due = float('inf')
|
||||||
|
|
||||||
|
urgency_threshold = 3
|
||||||
|
importance_threshold = 3
|
||||||
|
|
||||||
|
if time_until_due <= urgency_threshold and instance.importance >= importance_threshold:
|
||||||
|
instance.priority = Todo.EisenhowerMatrix.IMPORTANT_URGENT
|
||||||
|
elif time_until_due > urgency_threshold and instance.importance >= importance_threshold:
|
||||||
|
instance.priority = Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT
|
||||||
|
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
|
||||||
@ -1,21 +1,21 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from ..models import Task
|
from ..models import Todo
|
||||||
|
|
||||||
class TaskCreateSerializer(serializers.ModelSerializer):
|
class TaskCreateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Todo
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
exclude = ('tags',)
|
exclude = ('tags',)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# Create a new task with validated data
|
# Create a new task with validated data
|
||||||
return Task.objects.create(**validated_data)
|
return Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
class TaskGeneralSerializer(serializers.ModelSerializer):
|
class TaskGeneralSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Todo
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# Create a new task with validated data
|
# Create a new task with validated data
|
||||||
return Task.objects.create(**validated_data)
|
return Todo.objects.create(**validated_data)
|
||||||
@ -1,37 +1,16 @@
|
|||||||
from rest_framework import status
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.generics import CreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, DestroyAPIView
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from ..models import Task
|
from tasks.models import Todo
|
||||||
from .serializers import TaskCreateSerializer, TaskGeneralSerializer
|
from .serializers import TaskCreateSerializer, TaskGeneralSerializer
|
||||||
|
|
||||||
class TaskCreateView(CreateAPIView):
|
|
||||||
queryset = Task.objects.all()
|
|
||||||
serializer_class = TaskCreateSerializer
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
class TodoViewSet(viewsets.ModelViewSet):
|
||||||
serializer = self.get_serializer(data=request.data)
|
queryset = Todo.objects.all()
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
self.perform_create(serializer)
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
class TaskRetrieveView(RetrieveAPIView):
|
|
||||||
queryset = Task.objects.all()
|
|
||||||
serializer_class = TaskGeneralSerializer
|
serializer_class = TaskGeneralSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
class TaskUpdateView(RetrieveUpdateAPIView):
|
# Can't add ManytoMany at creation time (Tags)
|
||||||
queryset = Task.objects.all()
|
if self.action == 'create':
|
||||||
serializer_class = TaskGeneralSerializer
|
return TaskCreateSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
return TaskGeneralSerializer
|
||||||
|
|
||||||
|
|
||||||
class TaskDeleteView(DestroyAPIView):
|
|
||||||
queryset = Task.objects.all()
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
@ -5,8 +5,8 @@ from django.test import TestCase
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from tasks.tests.utils import create_test_user, login_user
|
from tasks.tests.utils import create_test_user, login_user
|
||||||
from tasks.serializers import TaskUpdateSerializer
|
from tasks.serializers import TodoUpdateSerializer
|
||||||
from tasks.models import Task
|
from tasks.models import Todo
|
||||||
|
|
||||||
class TaskUpdateSerializerTest(TestCase):
|
class TaskUpdateSerializerTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -25,14 +25,14 @@ class TaskUpdateSerializerTest(TestCase):
|
|||||||
'end_datetie': self.end_time,
|
'end_datetie': self.end_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer = TaskUpdateSerializer(data=data, user=self.user)
|
serializer = TodoUpdateSerializer(data=data, user=self.user)
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
task = serializer.save()
|
task = serializer.save()
|
||||||
self.assertIsInstance(task, Task)
|
self.assertIsInstance(task, Todo)
|
||||||
|
|
||||||
def test_serializer_update(self):
|
def test_serializer_update(self):
|
||||||
task = Task.objects.create(title='Original Task', notes='Original description', user=self.user)
|
task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'id': '32141cwaNcapufh8jq2conw',
|
'id': '32141cwaNcapufh8jq2conw',
|
||||||
@ -44,7 +44,7 @@ class TaskUpdateSerializerTest(TestCase):
|
|||||||
'end_datetie': self.end_time,
|
'end_datetie': self.end_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer = TaskUpdateSerializer(instance=task, data=data)
|
serializer = TodoUpdateSerializer(instance=task, data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
updated_task = serializer.save()
|
updated_task = serializer.save()
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from tasks.models import Task
|
|
||||||
from tasks.tests.utils import create_test_user
|
|
||||||
|
|
||||||
class TaskModelTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = create_test_user()
|
|
||||||
|
|
||||||
def test_eisenhower_matrix_category(self):
|
|
||||||
task = Task(importance=2, end_event=None, user=self.user)
|
|
||||||
task.save()
|
|
||||||
|
|
||||||
# 'Not Important & Not Urgent' category (category 4)
|
|
||||||
self.assertEqual(task.calculate_eisenhower_matrix_category(), 4)
|
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=1)
|
|
||||||
task = Task(importance=4, end_event=due_date, user=self.user)
|
|
||||||
task.save()
|
|
||||||
|
|
||||||
# 'Important & Urgent' category (category 1)
|
|
||||||
self.assertEqual(task.calculate_eisenhower_matrix_category(), 1)
|
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=10)
|
|
||||||
task = Task(importance=3, end_event=due_date, user=self.user)
|
|
||||||
task.save()
|
|
||||||
|
|
||||||
# 'Important & Not Urgent' category (category 2)
|
|
||||||
self.assertEqual(task.calculate_eisenhower_matrix_category(), 2)
|
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=4)
|
|
||||||
task = Task(importance=1, end_event=due_date, user=self.user)
|
|
||||||
task.save()
|
|
||||||
|
|
||||||
# 'Not Important & Urgent' category (category 3)
|
|
||||||
self.assertEqual(task.calculate_eisenhower_matrix_category(), 3)
|
|
||||||
@ -1,22 +1,19 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from tasks.tests.utils import create_test_user, login_user
|
from tasks.tests.utils import create_test_user, login_user
|
||||||
from ..models import Task
|
from tasks.models import Todo
|
||||||
|
|
||||||
class TaskCreateViewTests(APITestCase):
|
|
||||||
|
class TodoViewSetTests(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
self.client = login_user(self.user)
|
self.client = login_user(self.user)
|
||||||
self.url = reverse("add-task")
|
self.url = reverse("todo-list")
|
||||||
self.due_date = datetime.now() + timedelta(days=5)
|
self.due_date = datetime.now() + timedelta(days=5)
|
||||||
|
|
||||||
|
def test_create_valid_todo(self):
|
||||||
def test_create_valid_task(self):
|
|
||||||
"""
|
"""
|
||||||
Test creating a valid task using the API.
|
Test creating a valid task using the API.
|
||||||
"""
|
"""
|
||||||
@ -28,24 +25,23 @@ class TaskCreateViewTests(APITestCase):
|
|||||||
'priority': 1,
|
'priority': 1,
|
||||||
'difficulty': 1,
|
'difficulty': 1,
|
||||||
'user': self.user.id,
|
'user': self.user.id,
|
||||||
'end_event': self.due_date,
|
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
}
|
}
|
||||||
response = self.client.post(self.url, data, format='json')
|
response = self.client.post(self.url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Task.objects.count(), 1)
|
self.assertEqual(Todo.objects.count(), 1)
|
||||||
self.assertEqual(Task.objects.get().title, 'Test Task')
|
self.assertEqual(Todo.objects.get().title, 'Test Task')
|
||||||
|
|
||||||
def test_create_invalid_task(self):
|
def test_create_invalid_todo(self):
|
||||||
"""
|
"""
|
||||||
Test creating an invalid task using the API.
|
Test creating an invalid task using the API.
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
'type': 'invalid', # Invalid task type
|
'type': 'invalid', # Invalid task type
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(self.url, data, format='json')
|
response = self.client.post(self.url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(Task.objects.count(), 0) # No task should be created
|
self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
||||||
|
|
||||||
def test_missing_required_fields(self):
|
def test_missing_required_fields(self):
|
||||||
"""
|
"""
|
||||||
@ -55,10 +51,9 @@ class TaskCreateViewTests(APITestCase):
|
|||||||
'title': 'Incomplete Task',
|
'title': 'Incomplete Task',
|
||||||
'type': 'habit',
|
'type': 'habit',
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(self.url, data, format='json')
|
response = self.client.post(self.url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(Task.objects.count(), 0) # No task should be created
|
self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
||||||
|
|
||||||
def test_invalid_user_id(self):
|
def test_invalid_user_id(self):
|
||||||
"""
|
"""
|
||||||
@ -71,9 +66,8 @@ class TaskCreateViewTests(APITestCase):
|
|||||||
'priority': 1,
|
'priority': 1,
|
||||||
'difficulty': 1,
|
'difficulty': 1,
|
||||||
'user': 999, # Invalid user ID
|
'user': 999, # Invalid user ID
|
||||||
'end_event': self.due_date,
|
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(self.url, data, format='json')
|
response = self.client.post(self.url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(Task.objects.count(), 0) # No task should be created
|
self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
||||||
36
backend/tasks/tests/test_todo_eisenhower.py
Normal file
36
backend/tasks/tests/test_todo_eisenhower.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from django.test import TestCase
|
||||||
|
from tasks.models import Todo
|
||||||
|
from tasks.tests.utils import create_test_user
|
||||||
|
|
||||||
|
class TodoPriorityTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_test_user()
|
||||||
|
|
||||||
|
def test_priority_calculation(self):
|
||||||
|
# Important = 2, Till Due = none
|
||||||
|
todo = Todo(importance=2, end_event=None, user=self.user)
|
||||||
|
todo.save()
|
||||||
|
# 'Not Important & Not Urgent'
|
||||||
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
|
due_date = datetime.now(timezone.utc) + timedelta(days=1)
|
||||||
|
# Important = 4, Till Due = 1
|
||||||
|
todo = Todo(importance=4, end_event=due_date, user=self.user)
|
||||||
|
todo.save()
|
||||||
|
# 'Important & Urgent'
|
||||||
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT)
|
||||||
|
|
||||||
|
due_date = datetime.now(timezone.utc) + timedelta(days=10)
|
||||||
|
# Important = 3, Till Due = 10
|
||||||
|
todo = Todo(importance=3, end_event=due_date, user=self.user)
|
||||||
|
todo.save()
|
||||||
|
# 'Important & Not Urgent'
|
||||||
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
|
due_date = datetime.now(timezone.utc) + timedelta(days=2)
|
||||||
|
# Important = 1, Till Due = 2
|
||||||
|
todo = Todo(importance=1, end_event=due_date, user=self.user)
|
||||||
|
todo.save()
|
||||||
|
# 'Not Important & Urgent'
|
||||||
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT)
|
||||||
@ -1,7 +1,7 @@
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
from ..models import Task
|
from ..models import Todo
|
||||||
|
|
||||||
|
|
||||||
def create_test_user(email="testusertestuser@example.com", username="testusertestuser",
|
def create_test_user(email="testusertestuser@example.com", username="testusertestuser",
|
||||||
@ -61,4 +61,4 @@ def create_test_task(user, **kwargs):
|
|||||||
|
|
||||||
task_attributes = {**defaults, **kwargs}
|
task_attributes = {**defaults, **kwargs}
|
||||||
|
|
||||||
return Task.objects.create(user=user, **task_attributes)
|
return Todo.objects.create(user=user, **task_attributes)
|
||||||
@ -1,17 +1,17 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .api import GoogleCalendarEventViewset
|
|
||||||
from .tasks.views import TaskCreateView, TaskRetrieveView, TaskUpdateView, TaskDeleteView
|
from tasks.api import GoogleCalendarEventViewset
|
||||||
from .misc.views import TagViewSet
|
from tasks.tasks.views import TodoViewSet
|
||||||
|
from tasks.misc.views import TagViewSet
|
||||||
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
router.register(r'todo', TodoViewSet)
|
||||||
router.register(r'tags', TagViewSet)
|
router.register(r'tags', TagViewSet)
|
||||||
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
|
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('tasks/create/', TaskCreateView.as_view(), name="add-task"),
|
|
||||||
path('tasks/<int:pk>/', TaskRetrieveView.as_view(), name='retrieve-task'),
|
|
||||||
path('tasks/<int:pk>/update/', TaskUpdateView.as_view(), name='update-task'),
|
|
||||||
path('tasks/<int:pk>/delete/', TaskDeleteView.as_view(), name='delete-task'),
|
|
||||||
]
|
]
|
||||||
@ -4,3 +4,6 @@ from django.apps import AppConfig
|
|||||||
class UsersConfig(AppConfig):
|
class UsersConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'users'
|
name = 'users'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import users.signals
|
||||||
30
backend/users/migrations/0004_userstats.py
Normal file
30
backend/users/migrations/0004_userstats.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-06 05:30
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0003_customuser_profile_pic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserStats',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('health', models.IntegerField(default=100)),
|
||||||
|
('gold', models.FloatField(default=0.0)),
|
||||||
|
('experience', models.FloatField(default=0)),
|
||||||
|
('strength', models.IntegerField(default=1)),
|
||||||
|
('intelligence', models.IntegerField(default=1)),
|
||||||
|
('endurance', models.IntegerField(default=1)),
|
||||||
|
('perception', models.IntegerField(default=1)),
|
||||||
|
('luck', models.IntegerField(default=1)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,7 +1,11 @@
|
|||||||
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
|
||||||
from .managers import CustomAccountManager
|
from .managers import CustomAccountManager
|
||||||
|
|
||||||
@ -32,15 +36,33 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
# class UserStats(models.Model):
|
def random_luck():
|
||||||
# """
|
return random.randint(1, 50)
|
||||||
# Represents User Profiles and Attributes.
|
|
||||||
# Fields:
|
class UserStats(models.Model):
|
||||||
# - health: health points of the user.
|
"""
|
||||||
# - gold: gold points of the user.
|
Represents User Profiles and Attributes.
|
||||||
# - experience: experience points of the user.
|
Fields:
|
||||||
# """
|
- health: health points of the user.
|
||||||
# user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
|
- gold: gold points of the user.
|
||||||
# health = models.IntegerField(default=100)
|
- experience: experience points of the user.
|
||||||
# gold = models.IntegerField(default=0)
|
"""
|
||||||
# experience = models.FloatField(default=0)
|
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
|
||||||
|
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):
|
||||||
|
return (math.pow(self.experience, 2) // 225) + 1
|
||||||
9
backend/users/signals.py
Normal file
9
backend/users/signals.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from users.models import CustomUser, UserStats
|
||||||
|
|
||||||
|
@receiver(post_save, sender=CustomUser)
|
||||||
|
def create_user_stats(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
UserStats.objects.create(user=instance)
|
||||||
@ -10,12 +10,23 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@asseinfo/react-kanban": "^2.2.0",
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@fullcalendar/core": "^6.1.9",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.9",
|
||||||
|
"@fullcalendar/interaction": "^6.1.9",
|
||||||
|
"@fullcalendar/react": "^6.1.9",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.9",
|
||||||
"@mui/icons-material": "^5.14.15",
|
"@mui/icons-material": "^5.14.15",
|
||||||
"@mui/material": "^5.14.15",
|
"@mui/material": "^5.14.15",
|
||||||
"@mui/system": "^5.14.15",
|
"@mui/system": "^5.14.15",
|
||||||
"@react-oauth/google": "^0.11.1",
|
"@react-oauth/google": "^0.11.1",
|
||||||
|
"@syncfusion/ej2-base": "^23.1.41",
|
||||||
|
"@syncfusion/ej2-kanban": "^23.1.36",
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.5.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@ -23,6 +34,7 @@
|
|||||||
"gapi-script": "^1.2.0",
|
"gapi-script": "^1.2.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-bootstrap": "^2.9.1",
|
"react-bootstrap": "^2.9.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,26 +1,36 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import TestAuth from './components/testAuth';
|
import TestAuth from './components/testAuth';
|
||||||
import LoginPage from './components/authentication/LoginPage';
|
import LoginPage from './components/authentication/LoginPage';
|
||||||
import SignUpPage from './components/authentication/SignUpPage';
|
import SignUpPage from './components/authentication/SignUpPage';
|
||||||
import NavBar from './components/Nav/Navbar';
|
import NavBar from './components/nav/Navbar';
|
||||||
import Home from './components/Home';
|
import Home from './components/Home';
|
||||||
import ProfileUpdate from './components/ProfileUpdatePage'
|
import ProfileUpdate from './components/ProfileUpdatePage';
|
||||||
|
import Calendar from './components/calendar/calendar';
|
||||||
|
import KanbanBoard from './components/kanbanBoard/kanbanBoard';
|
||||||
|
import IconSideNav from './components/IconSideNav'; // Import IconSideNav
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className="App">
|
<div className='display: flex'>
|
||||||
<NavBar/>
|
<IconSideNav />
|
||||||
|
<div className='flex-1'>
|
||||||
|
<NavBar />
|
||||||
|
<div className='flex items-center justify-center'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home/>}/>
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/login" element={<LoginPage/>}/>
|
<Route path="/tasks" element={<KanbanBoard />} />
|
||||||
<Route path="/signup" element={<SignUpPage/>}/>
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/testAuth" element={<TestAuth/>}/>
|
<Route path="/signup" element={<SignUpPage />} />
|
||||||
<Route path="/update_profile" element={<ProfileUpdate/>}/>
|
<Route path="/testAuth" element={<TestAuth />} />
|
||||||
|
<Route path="/update_profile" element={<ProfileUpdate />} />
|
||||||
|
<Route path="/calendar" element={<Calendar />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
23
frontend/src/api/TaskApi.jsx
Normal file
23
frontend/src/api/TaskApi.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Create an Axios instance with common configurations
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: 'http://127.0.0.1:8000/api/',
|
||||||
|
timeout: 5000,
|
||||||
|
headers: {
|
||||||
|
'Authorization': "Bearer " + localStorage.getItem('access_token'),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTodoTasks = () => {
|
||||||
|
return axiosInstance
|
||||||
|
.get('todo/')
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { SiFramer, SiTailwindcss, SiReact, SiJavascript, SiCss3 } from "react-icons/si";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import homeLogo from "../assets/home.png";
|
import homeLogo from "../assets/home.png";
|
||||||
import calendarLogo from "../assets/calendar.png";
|
import calendarLogo from "../assets/calendar.png";
|
||||||
import planLogo from "../assets/planning.png";
|
import planLogo from "../assets/planning.png";
|
||||||
@ -8,18 +8,17 @@ import pieLogo from "../assets/pie-chart.png";
|
|||||||
import plusLogo from "../assets/plus.png";
|
import plusLogo from "../assets/plus.png";
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: 0, icon: <homeLogo />, logo: homeLogo },
|
{ id: 0, path: "/", icon: <homeLogo />, logo: homeLogo },
|
||||||
{ id: 1, icon: <calendarLogo />, logo: calendarLogo },
|
{ id: 1, path: "/tasks", icon: <planLogo />, logo: planLogo },
|
||||||
{ id: 2, icon: <planLogo />, logo: planLogo },
|
{ id: 2, path: "/calendar", icon: <calendarLogo />, logo: calendarLogo },
|
||||||
{ id: 3, icon: <pieLogo />, logo: pieLogo },
|
{ id: 3, path: "/pie", icon: <pieLogo />, logo: pieLogo },
|
||||||
{ id: 4, icon: <plusLogo />, logo: plusLogo },
|
{ id: 4, path: "/plus", icon: <plusLogo />, logo: plusLogo },
|
||||||
];
|
];
|
||||||
|
|
||||||
const IconSideNav = () => {
|
const IconSideNav = () => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-900 text-slate-100 flex">
|
<div className="bg-slate-900 text-slate-100 flex">
|
||||||
<SideNav />
|
<SideNav />
|
||||||
<div className="w-full"></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -28,7 +27,7 @@ const SideNav = () => {
|
|||||||
const [selected, setSelected] = useState(0);
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="h-[500px] w-fit bg-slate-950 p-4 flex flex-col items-center gap-2">
|
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-screen">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -37,17 +36,23 @@ const SideNav = () => {
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
logo={item.logo}
|
logo={item.logo}
|
||||||
|
path={item.path}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavItem = ({ icon, selected, id, setSelected, logo }) => {
|
const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
className="p-3 text-xl bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
|
className="p-3 text-xl bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
|
||||||
onClick={() => setSelected(id)}
|
onClick={() => {
|
||||||
|
setSelected(id);
|
||||||
|
navigate(path);
|
||||||
|
}}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import IsAuthenticated from '../authentication/IsAuthenticated';
|
import IsAuthenticated from '../authentication/IsAuthenticated';
|
||||||
import axiosapi from '../../api/axiosapi';
|
import axiosapi from '../../api/AuthenticationApi';
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useGoogleLogin } from "@react-oauth/google"
|
import { useGoogleLogin } from "@react-oauth/google"
|
||||||
|
|
||||||
import refreshAccessToken from './refreshAcesstoken';
|
import refreshAccessToken from './refreshAcesstoken';
|
||||||
import axiosapi from '../../api/axiosapi';
|
import axiosapi from '../../api/AuthenticationApi';
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import axiosapi from '../../api/axiosapi';
|
import axiosapi from '../../api/AuthenticationApi';
|
||||||
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|||||||
42
frontend/src/components/calendar/TaskDataHandler.jsx
Normal file
42
frontend/src/components/calendar/TaskDataHandler.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { fetchTodoTasks } from '../../api/TaskApi';
|
||||||
|
|
||||||
|
let eventGuid = 0
|
||||||
|
|
||||||
|
// function getDateAndTime(dateString) {
|
||||||
|
// const dateObject = new Date(dateString);
|
||||||
|
|
||||||
|
// const year = dateObject.getFullYear();
|
||||||
|
// const month = (dateObject.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
// const day = dateObject.getDate().toString().padStart(2, '0');
|
||||||
|
// const dateFormatted = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// const hours = dateObject.getUTCHours().toString().padStart(2, '0');
|
||||||
|
// const minutes = dateObject.getUTCMinutes().toString().padStart(2, '0');
|
||||||
|
// const seconds = dateObject.getUTCSeconds().toString().padStart(2, '0');
|
||||||
|
// const timeFormatted = `T${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// return dateFormatted + timeFormatted;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const mapResponseToEvents = (response) => {
|
||||||
|
return response.map(item => ({
|
||||||
|
id: createEventId(),
|
||||||
|
title: item.title,
|
||||||
|
start: item.start_event,
|
||||||
|
end: item.end_event,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEvents() {
|
||||||
|
try {
|
||||||
|
const response = await fetchTodoTasks();
|
||||||
|
return mapResponseToEvents(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEventId() {
|
||||||
|
return String(eventGuid++);
|
||||||
|
}
|
||||||
127
frontend/src/components/calendar/calendar.jsx
Normal file
127
frontend/src/components/calendar/calendar.jsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { formatDate } from "@fullcalendar/core";
|
||||||
|
import FullCalendar from "@fullcalendar/react";
|
||||||
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
|
import { getEvents, createEventId } from "./TaskDataHandler";
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
export default class Calendar extends React.Component {
|
||||||
|
state = {
|
||||||
|
weekendsVisible: true,
|
||||||
|
currentEvents: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="demo-app">
|
||||||
|
{this.renderSidebar()}
|
||||||
|
<div className="demo-app-main">
|
||||||
|
<FullCalendar
|
||||||
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
|
headerToolbar={{
|
||||||
|
left: "prev,next today",
|
||||||
|
center: "title",
|
||||||
|
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||||
|
}}
|
||||||
|
initialView="dayGridMonth"
|
||||||
|
editable={true}
|
||||||
|
selectable={true}
|
||||||
|
selectMirror={true}
|
||||||
|
dayMaxEvents={true}
|
||||||
|
weekends={this.state.weekendsVisible}
|
||||||
|
initialEvents={getEvents} // alternatively, use the `events` setting to fetch from a feed
|
||||||
|
select={this.handleDateSelect}
|
||||||
|
eventContent={renderEventContent} // custom render function
|
||||||
|
eventClick={this.handleEventClick}
|
||||||
|
eventsSet={this.handleEvents} // called after events are initialized/added/changed/removed
|
||||||
|
/* you can update a remote database when these fire:
|
||||||
|
eventAdd={function(){}}
|
||||||
|
eventChange={function(){}}
|
||||||
|
eventRemove={function(){}}
|
||||||
|
*/
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebar() {
|
||||||
|
return (
|
||||||
|
<div className="demo-app-sidebar">
|
||||||
|
<div className="demo-app-sidebar-section">
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Select dates and you will be prompted to create a new event</li>
|
||||||
|
<li>Drag, drop, and resize events</li>
|
||||||
|
<li>Click an event to delete it</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="demo-app-sidebar-section">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" checked={this.state.weekendsVisible} onChange={this.handleWeekendsToggle}></input>
|
||||||
|
toggle weekends
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="demo-app-sidebar-section">
|
||||||
|
<h2>All Events ({this.state.currentEvents.length})</h2>
|
||||||
|
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWeekendsToggle = () => {
|
||||||
|
this.setState({
|
||||||
|
weekendsVisible: !this.state.weekendsVisible,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDateSelect = selectInfo => {
|
||||||
|
let title = prompt("Please enter a new title for your event");
|
||||||
|
let calendarApi = selectInfo.view.calendar;
|
||||||
|
|
||||||
|
calendarApi.unselect(); // clear date selection
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
calendarApi.addEvent({
|
||||||
|
id: createEventId(),
|
||||||
|
title,
|
||||||
|
start: selectInfo.startStr,
|
||||||
|
end: selectInfo.endStr,
|
||||||
|
allDay: selectInfo.allDay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEventClick = clickInfo => {
|
||||||
|
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
|
||||||
|
clickInfo.event.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEvents = events => {
|
||||||
|
this.setState({
|
||||||
|
currentEvents: events,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEventContent(eventInfo) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<b>{eventInfo.timeText}</b>
|
||||||
|
<i>{eventInfo.event.title}</i>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSidebarEvent(event) {
|
||||||
|
return (
|
||||||
|
<li key={event.id}>
|
||||||
|
<b>{formatDate(event.start, { year: "numeric", month: "short", day: "numeric" })}</b>
|
||||||
|
<i>{event.title}</i>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
frontend/src/components/calendar/index.css
Normal file
55
frontend/src/components/calendar/index.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
body > div { /* the react root */
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
b { /* used for event dates/times */
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-app {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-app-sidebar {
|
||||||
|
width: 300px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #eaf9ff;
|
||||||
|
border-right: 1px solid #d3e2e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-app-sidebar-section {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-app-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc { /* the calendar root */
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
22
frontend/src/components/icons/plusIcon.jsx
Normal file
22
frontend/src/components/icons/plusIcon.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function PlusIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlusIcon;
|
||||||
23
frontend/src/components/icons/trashIcon.jsx
Normal file
23
frontend/src/components/icons/trashIcon.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function TrashIcon() {
|
||||||
|
return (
|
||||||
|
React.createElement(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
fill: "none",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
className: "w-6 h-6"
|
||||||
|
},
|
||||||
|
React.createElement("path", {
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrashIcon;
|
||||||
178
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal file
178
frontend/src/components/kanbanBoard/columnContainer.jsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
|
import TrashIcon from "../icons/trashIcon";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import PlusIcon from "../icons/plusIcon";
|
||||||
|
import TaskCard from "./taskCard";
|
||||||
|
|
||||||
|
function ColumnContainer({
|
||||||
|
column,
|
||||||
|
deleteColumn,
|
||||||
|
updateColumn,
|
||||||
|
createTask,
|
||||||
|
tasks,
|
||||||
|
deleteTask,
|
||||||
|
updateTask,
|
||||||
|
}) {
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
|
const tasksIds = useMemo(() => {
|
||||||
|
return tasks.map((task) => task.id);
|
||||||
|
}, [tasks]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: column.id,
|
||||||
|
data: {
|
||||||
|
type: "Column",
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
disabled: editMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transition,
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className="
|
||||||
|
bg-columnBackgroundColor
|
||||||
|
opacity-40
|
||||||
|
border-2
|
||||||
|
border-pink-500
|
||||||
|
w-[350px]
|
||||||
|
h-[500px]
|
||||||
|
max-h-[500px]
|
||||||
|
rounded-md
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className="
|
||||||
|
bg-columnBackgroundColor
|
||||||
|
w-[350px]
|
||||||
|
h-[500px]
|
||||||
|
max-h-[500px]
|
||||||
|
rounded-md
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{/* Column title */}
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
onClick={() => {
|
||||||
|
setEditMode(true);
|
||||||
|
}}
|
||||||
|
className="
|
||||||
|
bg-mainBackgroundColor
|
||||||
|
text-md
|
||||||
|
h-[60px]
|
||||||
|
cursor-grab
|
||||||
|
rounded-md
|
||||||
|
rounded-b-none
|
||||||
|
p-3
|
||||||
|
font-bold
|
||||||
|
border-columnBackgroundColor
|
||||||
|
border-4
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-between
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
flex
|
||||||
|
justify-center
|
||||||
|
items-center
|
||||||
|
bg-columnBackgroundColor
|
||||||
|
px-2
|
||||||
|
py-1
|
||||||
|
text-sm
|
||||||
|
rounded-full
|
||||||
|
"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
{!editMode && column.title}
|
||||||
|
{editMode && (
|
||||||
|
<input
|
||||||
|
className="bg-black focus:border-rose-500 border rounded outline-none px-2"
|
||||||
|
value={column.title}
|
||||||
|
onChange={(e) => updateColumn(column.id, e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
onBlur={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== "Enter") return;
|
||||||
|
setEditMode(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
deleteColumn(column.id);
|
||||||
|
}}
|
||||||
|
className="
|
||||||
|
stroke-gray-500
|
||||||
|
hover:stroke-white
|
||||||
|
hover:bg-columnBackgroundColor
|
||||||
|
rounded
|
||||||
|
px-1
|
||||||
|
py-2
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column task container */}
|
||||||
|
<div className="flex flex-grow flex-col gap-4 p-2 overflow-x-hidden overflow-y-auto">
|
||||||
|
<SortableContext items={tasksIds}>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<TaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</div>
|
||||||
|
{/* Column footer */}
|
||||||
|
<button
|
||||||
|
className="flex gap-2 items-center border-columnBackgroundColor border-2 rounded-md p-4 border-x-columnBackgroundColor hover:bg-mainBackgroundColor hover:text-rose-500 active:bg-black"
|
||||||
|
onClick={() => {
|
||||||
|
createTask(column.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
Add task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnContainer;
|
||||||
336
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal file
336
frontend/src/components/kanbanBoard/kanbanBoard.jsx
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import PlusIcon from "../icons/plusIcon"
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import ColumnContainer from "./columnContainer";
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragOverlay,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import TaskCard from "./taskCard";
|
||||||
|
|
||||||
|
const defaultCols = [
|
||||||
|
{
|
||||||
|
id: "todo",
|
||||||
|
title: "Todo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "doing",
|
||||||
|
title: "Work in progress",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "done",
|
||||||
|
title: "Done",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultTasks = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
columnId: "todo",
|
||||||
|
content: "List admin APIs for dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
columnId: "todo",
|
||||||
|
content:
|
||||||
|
"Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
columnId: "doing",
|
||||||
|
content: "Conduct security testing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
columnId: "doing",
|
||||||
|
content: "Analyze competitors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
columnId: "done",
|
||||||
|
content: "Create UI kit documentation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
columnId: "done",
|
||||||
|
content: "Dev meeting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
columnId: "done",
|
||||||
|
content: "Deliver dashboard prototype",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
columnId: "todo",
|
||||||
|
content: "Optimize application performance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
columnId: "todo",
|
||||||
|
content: "Implement data validation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
columnId: "todo",
|
||||||
|
content: "Design database schema",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
columnId: "todo",
|
||||||
|
content: "Integrate SSL web certificates into workflow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
columnId: "doing",
|
||||||
|
content: "Implement error logging and monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
columnId: "doing",
|
||||||
|
content: "Design and implement responsive UI",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function KanbanBoard() {
|
||||||
|
const [columns, setColumns] = useState(defaultCols);
|
||||||
|
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
|
||||||
|
|
||||||
|
const [tasks, setTasks] = useState(defaultTasks);
|
||||||
|
|
||||||
|
const [activeColumn, setActiveColumn] = useState(null);
|
||||||
|
|
||||||
|
const [activeTask, setActiveTask] = useState(null);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
m-auto
|
||||||
|
flex
|
||||||
|
w-full
|
||||||
|
items-center
|
||||||
|
overflow-x-auto
|
||||||
|
overflow-y-hidden
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
>
|
||||||
|
<div className="m-auto flex gap-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<SortableContext items={columnsId}>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<ColumnContainer
|
||||||
|
key={col.id}
|
||||||
|
column={col}
|
||||||
|
deleteColumn={deleteColumn}
|
||||||
|
updateColumn={updateColumn}
|
||||||
|
createTask={createTask}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
tasks={tasks.filter((task) => task.columnId === col.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
createNewColumn();
|
||||||
|
}}
|
||||||
|
className="
|
||||||
|
h-[60px]
|
||||||
|
w-[350px]
|
||||||
|
min-w-[350px]
|
||||||
|
cursor-pointer
|
||||||
|
rounded-lg
|
||||||
|
bg-mainBackgroundColor
|
||||||
|
border-2
|
||||||
|
border-columnBackgroundColor
|
||||||
|
p-4
|
||||||
|
ring-rose-500
|
||||||
|
hover:ring-2
|
||||||
|
flex
|
||||||
|
gap-2
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
Add Column
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<DragOverlay>
|
||||||
|
{activeColumn && (
|
||||||
|
<ColumnContainer
|
||||||
|
column={activeColumn}
|
||||||
|
deleteColumn={deleteColumn}
|
||||||
|
updateColumn={updateColumn}
|
||||||
|
createTask={createTask}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
tasks={tasks.filter(
|
||||||
|
(task) => task.columnId === activeColumn.id
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTask && (
|
||||||
|
<TaskCard
|
||||||
|
task={activeTask}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function createTask(columnId) {
|
||||||
|
const newTask = {
|
||||||
|
id: generateId(),
|
||||||
|
columnId,
|
||||||
|
content: `Task ${tasks.length + 1}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTasks([...tasks, newTask]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTask(id) {
|
||||||
|
const newTasks = tasks.filter((task) => task.id !== id);
|
||||||
|
setTasks(newTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTask(id, content) {
|
||||||
|
const newTasks = tasks.map((task) => {
|
||||||
|
if (task.id !== id) return task;
|
||||||
|
return { ...task, content };
|
||||||
|
});
|
||||||
|
|
||||||
|
setTasks(newTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewColumn() {
|
||||||
|
const columnToAdd = {
|
||||||
|
id: generateId(),
|
||||||
|
title: `Column ${columns.length + 1}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
setColumns([...columns, columnToAdd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteColumn(id) {
|
||||||
|
const filteredColumns = columns.filter((col) => col.id !== id);
|
||||||
|
setColumns(filteredColumns);
|
||||||
|
|
||||||
|
const newTasks = tasks.filter((t) => t.columnId !== id);
|
||||||
|
setTasks(newTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColumn(id, title) {
|
||||||
|
const newColumns = columns.map((col) => {
|
||||||
|
if (col.id !== id) return col;
|
||||||
|
return { ...col, title };
|
||||||
|
});
|
||||||
|
|
||||||
|
setColumns(newColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(event) {
|
||||||
|
if (event.active.data.current?.type === "Column") {
|
||||||
|
setActiveColumn(event.active.data.current.column);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.active.data.current?.type === "Task") {
|
||||||
|
setActiveTask(event.active.data.current.task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd(event) {
|
||||||
|
setActiveColumn(null);
|
||||||
|
setActiveTask(null);
|
||||||
|
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
const activeId = active.id;
|
||||||
|
const overId = over.id;
|
||||||
|
|
||||||
|
if (activeId === overId) return;
|
||||||
|
|
||||||
|
const isActiveAColumn = active.data.current?.type === "Column";
|
||||||
|
if (!isActiveAColumn) return;
|
||||||
|
|
||||||
|
setColumns((columns) => {
|
||||||
|
const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
|
||||||
|
|
||||||
|
const overColumnIndex = columns.findIndex((col) => col.id === overId);
|
||||||
|
|
||||||
|
return arrayMove(columns, activeColumnIndex, overColumnIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(event) {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
const activeId = active.id;
|
||||||
|
const overId = over.id;
|
||||||
|
|
||||||
|
if (activeId === overId) return;
|
||||||
|
|
||||||
|
const isActiveATask = active.data.current?.type === "Task";
|
||||||
|
const isOverATask = over.data.current?.type === "Task";
|
||||||
|
|
||||||
|
if (!isActiveATask) return;
|
||||||
|
|
||||||
|
if (isActiveATask && isOverATask) {
|
||||||
|
setTasks((tasks) => {
|
||||||
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
|
const overIndex = tasks.findIndex((t) => t.id === overId);
|
||||||
|
|
||||||
|
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
|
||||||
|
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
||||||
|
return arrayMove(tasks, activeIndex, overIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayMove(tasks, activeIndex, overIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOverAColumn = over.data.current?.type === "Column";
|
||||||
|
|
||||||
|
if (isActiveATask && isOverAColumn) {
|
||||||
|
setTasks((tasks) => {
|
||||||
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
|
|
||||||
|
tasks[activeIndex].columnId = overId;
|
||||||
|
return arrayMove(tasks, activeIndex, activeIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateId() {
|
||||||
|
return Math.floor(Math.random() * 10001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KanbanBoard;
|
||||||
111
frontend/src/components/kanbanBoard/taskCard.jsx
Normal file
111
frontend/src/components/kanbanBoard/taskCard.jsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import TrashIcon from "../icons/trashIcon";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
function TaskCard({ task, deleteTask, updateTask }) {
|
||||||
|
const [mouseIsOver, setMouseIsOver] = useState(false);
|
||||||
|
const [editMode, setEditMode] = useState(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: task.id,
|
||||||
|
data: {
|
||||||
|
type: "Task",
|
||||||
|
task,
|
||||||
|
},
|
||||||
|
disabled: editMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transition,
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditMode = () => {
|
||||||
|
setEditMode((prev) => !prev);
|
||||||
|
setMouseIsOver(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className="
|
||||||
|
opacity-30
|
||||||
|
bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl border-2 border-rose-500 cursor-grab relative
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="
|
||||||
|
h-[90%]
|
||||||
|
w-full resize-none border-none rounded bg-transparent text-white focus:outline-none
|
||||||
|
"
|
||||||
|
value={task.content}
|
||||||
|
autoFocus
|
||||||
|
placeholder="Task content here"
|
||||||
|
onBlur={toggleEditMode}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && e.shiftKey) {
|
||||||
|
toggleEditMode();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => updateTask(task.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
onClick={toggleEditMode}
|
||||||
|
className="bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative task"
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setMouseIsOver(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setMouseIsOver(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="my-auto h-[90%] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap">
|
||||||
|
{task.content}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{mouseIsOver && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
deleteTask(task.id);
|
||||||
|
}}
|
||||||
|
className="stroke-white absolute right-4 top-1/2 -translate-y-1/2 bg-columnBackgroundColor p-2 rounded opacity-60 hover:opacity-100"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskCard;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import axiosapi from '../api/axiosapi';
|
import axiosapi from '../api/AuthenticationApi';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user