diff --git a/backend/tasks/api.py b/backend/tasks/api.py
index 7669d76..587f433 100644
--- a/backend/tasks/api.py
+++ b/backend/tasks/api.py
@@ -6,46 +6,22 @@ from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
-from tasks.utils import get_service
+from tasks.utils import get_service, generate_recurrence_rule
from tasks.models import Todo, RecurrenceTask
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
-
class GoogleCalendarEventViewset(viewsets.ViewSet):
+ """Viewset for list or save Google Calendar Events."""
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super().__init__()
- self.current_time = datetime.now(tz=timezone.utc).isoformat()
- self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)'
+ self.current_time = (datetime.now(tz=timezone.utc) + timedelta(days=-7)).isoformat()
+ self.max_time = (datetime.now(tz=timezone.utc) + timedelta(days=7)).isoformat()
+ self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end,originalStartTime)'
- def _validate_serializer(self, serializer):
- if serializer.is_valid():
- serializer.save()
- return Response("Validate Successfully", status=200)
- return Response(serializer.errors, status=400)
-
- def post(self, request):
- service = get_service(request)
- events = service.events().list(calendarId='primary', fields=self.event_fields).execute()
- 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:
- task = Todo.objects.get(google_calendar_id=event['id'])
- serializer = TodoUpdateSerializer(instance=task, data=event)
- return self._validate_serializer(serializer)
- except Todo.DoesNotExist:
- serializer = TodoUpdateSerializer(data=event, user=request.user)
- return self._validate_serializer(serializer)
-
- def list(self, request, days=7):
- max_time = (datetime.now(tz=timezone.utc) + timedelta(days=days)).isoformat()
-
+ def _get_google_events(self, request):
+ """Get all events from Google Calendar. """
service = get_service(request)
events = []
next_page_token = None
@@ -54,21 +30,77 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
query = service.events().list(
calendarId='primary',
timeMin=self.current_time,
- timeMax=max_time,
+ timeMax=self.max_time,
maxResults=200,
singleEvents=True,
orderBy='startTime',
pageToken=next_page_token,
- fields='items(id,summary,description,created,recurringEventId,updated,start,end)',
+ fields=self.event_fields,
)
page_results = query.execute()
page_events = page_results.get('items', [])
-
+
events.extend(page_events)
next_page_token = page_results.get('nextPageToken')
if next_page_token is None:
break
- return Response(events, status=200)
\ No newline at end of file
+ return events
+
+ def _validate_serializer(self, serializer):
+ """Validate serializer and return response."""
+ if serializer.is_valid():
+ serializer.save()
+ return Response("Validate Successfully", status=200)
+ return Response(serializer.errors, status=400)
+
+ def create(self, request, *args, **kwargs):
+ """Create a new Google Calendar Event."""
+ 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')
+
+ event['start_datetime'] = start_datetime
+ event['end_datetime'] = end_datetime
+ 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)
+ except Todo.DoesNotExist:
+ serializer = TodoUpdateSerializer(data=event, user=request.user)
+
+ responses.append(self._validate_serializer(serializer))
+
+ return responses[0] if responses else Response("No events to process", status=200)
+
+ def list(self, request):
+ """List all Google Calendar Events."""
+ return Response(self._get_google_events(request), status=200)
+
\ No newline at end of file
diff --git a/backend/tasks/migrations/0012_habit.py b/backend/tasks/migrations/0012_habit.py
new file mode 100644
index 0000000..6e29775
--- /dev/null
+++ b/backend/tasks/migrations/0012_habit.py
@@ -0,0 +1,39 @@
+# Generated by Django 4.2.6 on 2023-11-13 18:15
+
+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', '0011_recurrencetask'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Habit',
+ 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)),
+ ('streak', models.IntegerField(default=0)),
+ ('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,
+ },
+ ),
+ ]
diff --git a/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py b/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py
new file mode 100644
index 0000000..f629f13
--- /dev/null
+++ b/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.6 on 2023-11-14 15:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tasks', '0012_habit'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='recurrencetask',
+ name='recurrence_rule',
+ field=models.CharField(),
+ ),
+ ]
diff --git a/backend/tasks/models.py b/backend/tasks/models.py
index 5da815e..a8fc4e5 100644
--- a/backend/tasks/models.py
+++ b/backend/tasks/models.py
@@ -68,11 +68,19 @@ class Todo(Task):
return self.title
class RecurrenceTask(Task):
- recurrence_rule = models.TextField()
+ recurrence_rule = models.CharField()
def __str__(self) -> str:
return f"{self.title} ({self.recurrence_rule})"
+
+class Habit(Task):
+ streak = models.IntegerField(default=0)
+
+ def __str__(self) -> str:
+ return f"{self.title} ({self.streak})"
+
+
class Subtask(models.Model):
"""
Represents a subtask associated with a task.
diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py
index ed02300..408cb55 100644
--- a/backend/tasks/serializers.py
+++ b/backend/tasks/serializers.py
@@ -41,7 +41,7 @@ class RecurrenceTaskUpdateSerializer(serializers.ModelSerializer):
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")
+ recurrence = serializers.CharField(source="recurrence_rule")
start_datetime = serializers.DateTimeField(source="start_event", required=False)
end_datetime = serializers.DateTimeField(source="end_event", required=False)
diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py
index 85f0281..3098493 100644
--- a/backend/tasks/tasks/serializers.py
+++ b/backend/tasks/tasks/serializers.py
@@ -1,21 +1,47 @@
from rest_framework import serializers
-from ..models import Todo
+from ..models import Todo, RecurrenceTask, Habit
-class TaskCreateSerializer(serializers.ModelSerializer):
- class Meta:
- model = Todo
- # fields = '__all__'
- exclude = ('tags',)
-
- def create(self, validated_data):
- # Create a new task with validated data
- return Todo.objects.create(**validated_data)
-
-class TaskGeneralSerializer(serializers.ModelSerializer):
+class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
- return Todo.objects.create(**validated_data)
\ No newline at end of file
+ return Todo.objects.create(**validated_data)
+
+class TaskCreateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Todo
+ exclude = ('tags',)
+
+
+class RecurrenceTaskSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RecurrenceTask
+ fields = '__all__'
+
+ def create(self, validated_data):
+ # Create a new task with validated data
+ return Todo.objects.create(**validated_data)
+
+class RecurrenceTaskCreateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RecurrenceTask
+ exclude = ('tags',)
+
+
+class HabitTaskSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Habit
+ fields = '__all__'
+
+ def create(self, validated_data):
+ # Create a new task with validated data
+ return Todo.objects.create(**validated_data)
+
+
+class HabitTaskCreateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Habit
+ exclude = ('tags',)
\ No newline at end of file
diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py
index d884bbe..87bbaca 100644
--- a/backend/tasks/tasks/views.py
+++ b/backend/tasks/tasks/views.py
@@ -1,16 +1,49 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
-from tasks.models import Todo
-from .serializers import TaskCreateSerializer, TaskGeneralSerializer
+from tasks.models import Todo, RecurrenceTask, Habit
+from tasks.tasks.serializers import (TaskCreateSerializer,
+ TaskSerializer,
+ RecurrenceTaskSerializer,
+ RecurrenceTaskCreateSerializer,
+ HabitTaskSerializer,
+ HabitTaskCreateSerializer)
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
- serializer_class = TaskGeneralSerializer
+ serializer_class = TaskSerializer
permission_classes = [IsAuthenticated]
+ def get_queryset(self):
+ queryset = Todo.objects.filter(user=self.request.user)
+ return queryset
+
def get_serializer_class(self):
# Can't add ManytoMany at creation time (Tags)
if self.action == 'create':
return TaskCreateSerializer
- return TaskGeneralSerializer
\ No newline at end of file
+ return TaskSerializer
+
+
+class RecurrenceTaskViewSet(viewsets.ModelViewSet):
+ queryset = RecurrenceTask.objects.all()
+ serializer_class = RecurrenceTaskSerializer
+ permission_classes = [IsAuthenticated]
+
+ def get_serializer_class(self):
+ # Can't add ManytoMany at creation time (Tags)
+ if self.action == 'create':
+ return RecurrenceTaskCreateSerializer
+ return RecurrenceTaskSerializer
+
+
+class HabitTaskViewSet(viewsets.ModelViewSet):
+ queryset = Habit.objects.all()
+ serializer_class = HabitTaskSerializer
+ permission_classes = [IsAuthenticated]
+
+ def get_serializer_class(self):
+ # Can't add ManytoMany at creation time (Tags)
+ if self.action == 'create':
+ return HabitTaskCreateSerializer
+ return HabitTaskSerializer
\ No newline at end of file
diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py
index b44ddd9..d830a65 100644
--- a/backend/tasks/urls.py
+++ b/backend/tasks/urls.py
@@ -3,12 +3,14 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter
from tasks.api import GoogleCalendarEventViewset
-from tasks.tasks.views import TodoViewSet
+from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet
from tasks.misc.views import TagViewSet
router = DefaultRouter()
router.register(r'todo', TodoViewSet)
+router.register(r'daily', RecurrenceTaskViewSet)
+router.register(r'habit', HabitTaskViewSet)
router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
diff --git a/backend/tasks/utils.py b/backend/tasks/utils.py
index c55eb4a..de52535 100644
--- a/backend/tasks/utils.py
+++ b/backend/tasks/utils.py
@@ -1,8 +1,55 @@
+from dateutil import rrule
+from datetime import datetime
+
from googleapiclient.discovery import build
from authentications.access_token_cache import get_credential_from_cache_token
def get_service(request):
+ """
+ Get a service that communicates to a Google API.
+
+ :param request: Http request object
+ :return: A Resource object with methods for interacting with the calendar service
+ """
credentials = get_credential_from_cache_token(request.user.id)
- return build('calendar', 'v3', credentials=credentials)
\ No newline at end of file
+ return build('calendar', 'v3', credentials=credentials)
+
+def _determine_frequency(time_difference):
+ if time_difference.days >= 365:
+ return rrule.YEARLY
+ elif time_difference.days >= 30:
+ return rrule.MONTHLY
+ elif time_difference.days >= 7:
+ return rrule.WEEKLY
+ elif time_difference.days >= 1:
+ return rrule.DAILY
+ elif time_difference.seconds >= 3600:
+ return rrule.HOURLY
+ elif time_difference.seconds >= 60:
+ return rrule.MINUTELY
+ else:
+ return rrule.SECONDLY
+
+def generate_recurrence_rule(datetime1: str, datetime2: str, original_start_time: str) -> str:
+ """
+ Generate recurrence rule from
+ difference between two datetime string.
+
+ :param task1: A task object
+ :param task2: A task object
+ :return: A recurrence rule string according to ICAL format
+ """
+ start_time1 = datetime.fromisoformat(datetime1)
+ start_time2 = datetime.fromisoformat(datetime2)
+
+ time_difference = start_time2 - start_time1
+
+ recurrence_rule = rrule.rrule(
+ freq=_determine_frequency(time_difference),
+ dtstart=datetime.fromisoformat(original_start_time),
+ interval=time_difference.days if time_difference.days > 0 else 1,
+ )
+
+ return str(recurrence_rule)
\ No newline at end of file
diff --git a/backend/users/migrations/0005_alter_userstats_endurance_and_more.py b/backend/users/migrations/0005_alter_userstats_endurance_and_more.py
new file mode 100644
index 0000000..35fe6f7
--- /dev/null
+++ b/backend/users/migrations/0005_alter_userstats_endurance_and_more.py
@@ -0,0 +1,40 @@
+# Generated by Django 4.2.6 on 2023-11-13 18:15
+
+import django.core.validators
+from django.db import migrations, models
+import users.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0004_userstats'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userstats',
+ name='endurance',
+ field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
+ ),
+ migrations.AlterField(
+ model_name='userstats',
+ name='intelligence',
+ field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
+ ),
+ migrations.AlterField(
+ model_name='userstats',
+ name='luck',
+ field=models.IntegerField(default=users.models.random_luck, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]),
+ ),
+ migrations.AlterField(
+ model_name='userstats',
+ name='perception',
+ field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
+ ),
+ migrations.AlterField(
+ model_name='userstats',
+ name='strength',
+ field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
+ ),
+ ]
diff --git a/frontend/src/App.css b/frontend/src/App.css
index eaac616..48931c9 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/App.jsx b/frontend/src/App.jsx
index 39f5ec7..ad9dc97 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -4,13 +4,15 @@ import { Route, Routes, useLocation } from "react-router-dom";
import TestAuth from "./components/testAuth";
import LoginPage from "./components/authentication/LoginPage";
import SignUpPage from "./components/authentication/SignUpPage";
-import NavBar from "./components/navigators/Navbar";
+import NavBar from "./components/navigations/Navbar";
import Home from "./components/Home";
-import ProfileUpdate from "./components/ProfileUpdatePage";
import Calendar from "./components/calendar/calendar";
-import KanbanBoard from "./components/kanbanBoard/kanbanBoard";
-import IconSideNav from "./components/IconSideNav";
+import KanbanPage from "./components/kanbanBoard/kanbanPage";
+import IconSideNav from "./components/navigations/IconSideNav";
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
+import PrivateRoute from "./PrivateRoute";
+import ProfileUpdatePage from "./components/profilePage";
+
const App = () => {
const location = useLocation();
@@ -18,24 +20,32 @@ const App = () => {
const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_));
return (
-
- {!isLoginPageOrSignUpPage &&
}
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
+ {!isLoginPageOrSignUpPage &&
}
+
+
+
+
+ } />
+ }>
+ } />
+
+ } />
+ }>
+ } />
+
+ }>
+ } />
+
+ }>
+ } />
+
+ } />
+ } />
+
+
);
};
diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx
new file mode 100644
index 0000000..d72491a
--- /dev/null
+++ b/frontend/src/PrivateRoute.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { Navigate, Outlet } from "react-router-dom";
+import { useAuth } from "./hooks/authentication/IsAuthenticated";
+
+const PrivateRoute = () => {
+ const { isAuthenticated, setIsAuthenticated } = useAuth();
+ const auth = isAuthenticated;
+ return auth ?
:
;
+};
+
+export default PrivateRoute;
diff --git a/frontend/src/api/TagApi.jsx b/frontend/src/api/TagApi.jsx
index deea971..5d352aa 100644
--- a/frontend/src/api/TagApi.jsx
+++ b/frontend/src/api/TagApi.jsx
@@ -1,12 +1,8 @@
-import axiosInstance from "./configs/AxiosConfig";
+import { createTask, readTasks, readTaskByID, updateTask, deleteTask } from "./TaskApi";
-export const fetchTags = () => {
- return axiosInstance
- .get("tags/")
- .then(response => {
- return response.data;
- })
- .catch(error => {
- throw error;
- });
-};
+// CRUD functions for "tags" endpoint
+export const createTag = data => createTask("tags", data);
+export const readTags = () => readTasks("tags");
+export const readTagByID = id => readTaskByID("tags", id);
+export const updateTag = (id, data) => updateTask("tags", id, data);
+export const deleteTag = id => deleteTask("tags", id);
diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx
index 96f24f6..eee70d0 100644
--- a/frontend/src/api/TaskApi.jsx
+++ b/frontend/src/api/TaskApi.jsx
@@ -1,23 +1,73 @@
import axiosInstance from "./configs/AxiosConfig";
-export const fetchTodoTasks = () => {
+const baseURL = "";
+
+export const createTask = (endpoint, data) => {
return axiosInstance
- .get("todo/")
- .then(response => {
- return response.data;
- })
+ .post(`${baseURL}${endpoint}/`, data)
+ .then(response => response.data)
.catch(error => {
throw error;
});
};
-export const fetchTodoTasksID = id => {
+export const readTasks = endpoint => {
return axiosInstance
- .get(`todo/${id}/`)
- .then(response => {
- return response.data;
- })
+ .get(`${baseURL}${endpoint}/`)
+ .then(response => response.data)
.catch(error => {
throw error;
});
};
+
+export const readTaskByID = (endpoint, id) => {
+ return axiosInstance
+ .get(`${baseURL}${endpoint}/${id}/`)
+ .then(response => response.data)
+ .catch(error => {
+ throw error;
+ });
+};
+
+export const updateTask = (endpoint, id, data) => {
+ return axiosInstance
+ .put(`${baseURL}${endpoint}/${id}/`, data)
+ .then(response => response.data)
+ .catch(error => {
+ throw error;
+ });
+};
+
+export const deleteTask = (endpoint, id) => {
+ return axiosInstance
+ .delete(`${baseURL}${endpoint}/${id}/`)
+ .then(response => response.data)
+ .catch(error => {
+ throw error;
+ });
+};
+
+// Create
+export const createTodoTask = data => createTask("todo", data);
+export const createRecurrenceTask = data => createTask("daily", data);
+export const createHabitTask = data => createTask("habit", data);
+
+// Read
+export const readTodoTasks = () => readTasks("todo");
+export const readRecurrenceTasks = () => readTasks("daily");
+export const readHabitTasks = () => readTasks("habit");
+
+// Read by ID
+export const readTodoTaskByID = id => readTaskByID("todo", id);
+export const readRecurrenceTaskByID = id => readTaskByID("daily", id);
+export const readHabitTaskByID = id => readTaskByID("habit", id);
+
+// Update
+export const updateTodoTask = (id, data) => updateTask("todo", id, data);
+export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
+export const updateHabitTask = (id, data) => updateTask("habit", id, data);
+
+// Delete
+export const deleteTodoTask = id => deleteTask("todo", id);
+export const deleteRecurrenceTask = id => deleteTask("daily", id);
+export const deleteHabitTask = id => deleteTask("habit", id);
diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx
index ca80fd1..e7a1b17 100644
--- a/frontend/src/api/UserProfileApi.jsx
+++ b/frontend/src/api/UserProfileApi.jsx
@@ -1,11 +1,11 @@
-import axios from 'axios';
+import axios from "axios";
-const ApiUpdateUserProfile = async (formData) => {
+const ApiUpdateUserProfile = async formData => {
try {
- const response = await axios.post('http://127.0.1:8000/api/user/update/', formData, {
+ const response = await axios.post("http://127.0.1:8000/api/user/update/", formData, {
headers: {
- 'Authorization': "Bearer " + localStorage.getItem('access_token'),
- 'Content-Type': 'multipart/form-data',
+ Authorization: "Bearer " + localStorage.getItem("access_token"),
+ "Content-Type": "multipart/form-data",
},
});
@@ -13,7 +13,7 @@ const ApiUpdateUserProfile = async (formData) => {
return response.data;
} catch (error) {
- console.error('Error updating user profile:', error);
+ console.error("Error updating user profile:", error);
throw error;
}
};
diff --git a/frontend/src/api/configs/AxiosConfig.jsx b/frontend/src/api/configs/AxiosConfig.jsx
index 80015ac..b0410d1 100644
--- a/frontend/src/api/configs/AxiosConfig.jsx
+++ b/frontend/src/api/configs/AxiosConfig.jsx
@@ -1,42 +1,45 @@
-import axios from 'axios';
+import axios from "axios";
+import { redirect } from "react-router-dom";
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',
- },
+ baseURL: "http://127.0.0.1:8000/api/",
+ timeout: 5000,
+ headers: {
+ Authorization: "Bearer " + localStorage.getItem("access_token"),
+ "Content-Type": "application/json",
+ accept: "application/json",
+ },
});
// handling token refresh on 401 Unauthorized errors
axiosInstance.interceptors.response.use(
- response => response,
- error => {
- const originalRequest = error.config;
- const refresh_token = localStorage.getItem('refresh_token');
+ response => response,
+ error => {
+ const originalRequest = error.config;
+ const refresh_token = localStorage.getItem("refresh_token");
- // Check if the error is due to 401 and a refresh token is available
- if (error.response.status === 401 && error.response.statusText === "Unauthorized" && refresh_token !== "undefined") {
- return axiosInstance
- .post('/token/refresh/', { refresh: refresh_token })
- .then((response) => {
+ // Check if the error is due to 401 and a refresh token is available
+ if (
+ error.response.status === 401 &&
+ error.response.statusText === "Unauthorized" &&
+ refresh_token !== "undefined"
+ ) {
+ return axiosInstance
+ .post("/token/refresh/", { refresh: refresh_token })
+ .then(response => {
+ localStorage.setItem("access_token", response.data.access);
- localStorage.setItem('access_token', response.data.access);
+ axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access;
+ originalRequest.headers["Authorization"] = "Bearer " + response.data.access;
- axiosInstance.defaults.headers['Authorization'] = "Bearer " + response.data.access;
- originalRequest.headers['Authorization'] = "Bearer " + response.data.access;
-
- return axiosInstance(originalRequest);
- })
- .catch(err => {
- console.log('Interceptors error: ', err);
- });
- }
- return Promise.reject(error);
+ return axiosInstance(originalRequest);
+ })
+ .catch(err => {
+ console.log("Interceptors error: ", err);
+ });
}
+ return Promise.reject(error);
+ }
);
-
export default axiosInstance;
diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx
index 7800fa9..7c31328 100644
--- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx
+++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx
@@ -1,27 +1,77 @@
-import React from 'react';
+import React, { useState } from "react";
+import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi";
+
+function EachBlog({ name, colorCode, contentList, icon }) {
+ const [tasks, setTasks] = useState(contentList);
+
+ const handleCheckboxChange = index => {
+ const updatedTasks = [...tasks];
+ updatedTasks[index].checked = !updatedTasks[index].checked;
+ setTasks(updatedTasks);
+ };
-function EachBlog({ name, colorCode }) {
return (
-
-
- {name}
+
+
+ {icon}
+ {name}
-
- Content goes here
+
+
+ {tasks.length === 0 ? (
+
No tasks
+ ) : (
+ tasks.map((item, index) => (
+
+ handleCheckboxChange(index)}
+ />
+
+
+ ))
+ )}
);
}
function Eisenhower() {
+ const contentList_ui = [
+ { text: "Complete report for the meeting", checked: true },
+ { text: "Review project proposal", checked: false },
+ { text: "Submit expense report", checked: false },
+ { text: "Complete report for the meeting", checked: true },
+ { text: "Review project proposal", checked: false },
+ { text: "Submit expense report", checked: false },
+ { text: "Complete report for the meeting", checked: true },
+ { text: "Review project proposal", checked: false },
+ { text: "Submit expense report", checked: false },
+ ];
+
+ const contentList_uni = [];
+ const contentList_nui = [];
+ const contentList_nuni = [];
+
return (
-
-
The Eisenhower Matrix
-
-
-
-
-
+
+
+ } contentList={contentList_ui} />
+ } contentList={contentList_uni} />
+ }
+ contentList={contentList_nui}
+ />
+ }
+ contentList={contentList_nuni}
+ />
);
diff --git a/frontend/src/components/Home.jsx b/frontend/src/components/Home.jsx
index 3089df5..7fa7680 100644
--- a/frontend/src/components/Home.jsx
+++ b/frontend/src/components/Home.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React from "react";
function HomePage() {
return (
diff --git a/frontend/src/components/ProfileUpdatePage.jsx b/frontend/src/components/ProfileUpdateComponent.jsx
similarity index 73%
rename from frontend/src/components/ProfileUpdatePage.jsx
rename to frontend/src/components/ProfileUpdateComponent.jsx
index 06a0213..74da41f 100644
--- a/frontend/src/components/ProfileUpdatePage.jsx
+++ b/frontend/src/components/ProfileUpdateComponent.jsx
@@ -1,12 +1,12 @@
-import React, { useState, useRef } from 'react';
-import { ApiUpdateUserProfile } from '../api/UserProfileApi';
+import React, { useState, useRef } from "react";
+import { ApiUpdateUserProfile } from "../api/UserProfileApi";
-function ProfileUpdate() {
+function ProfileUpdateComponent() {
const [file, setFile] = useState(null);
- const [username, setUsername] = useState('');
- const [fullName, setFullName] = useState('');
- const [about, setAbout] = useState('');
- const defaultImage = 'https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg';
+ const [username, setUsername] = useState("");
+ const [fullName, setFullName] = useState("");
+ const [about, setAbout] = useState("");
+ const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg";
const fileInputRef = useRef(null);
const handleImageUpload = () => {
@@ -15,7 +15,7 @@ function ProfileUpdate() {
}
};
- const handleFileChange = (e) => {
+ const handleFileChange = e => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setFile(selectedFile);
@@ -24,9 +24,9 @@ function ProfileUpdate() {
const handleSave = () => {
const formData = new FormData();
- formData.append('profile_pic', file);
- formData.append('first_name', username);
- formData.append('about', about);
+ formData.append("profile_pic", file);
+ formData.append("first_name", username);
+ formData.append("about", about);
ApiUpdateUserProfile(formData);
};
@@ -45,10 +45,7 @@ function ProfileUpdate() {
ref={fileInputRef}
/>
-
+
{file ? (
})
) : (
@@ -69,7 +66,7 @@ function ProfileUpdate() {
placeholder="Enter your username"
className="input w-full"
value={username}
- onChange={(e) => setUsername(e.target.value)}
+ onChange={e => setUsername(e.target.value)}
/>
@@ -81,7 +78,7 @@ function ProfileUpdate() {
placeholder="Enter your full name"
className="input w-full"
value={fullName}
- onChange={(e) => setFullName(e.target.value)}
+ onChange={e => setFullName(e.target.value)}
/>
@@ -92,7 +89,7 @@ function ProfileUpdate() {
placeholder="Tell us about yourself"
className="textarea w-full h-32"
value={about}
- onChange={(e) => setAbout(e.target.value)}
+ onChange={e => setAbout(e.target.value)}
/>
@@ -104,4 +101,4 @@ function ProfileUpdate() {
);
}
-export default ProfileUpdate;
+export default ProfileUpdateComponent;
diff --git a/frontend/src/components/authentication/IsAuthenticated.jsx b/frontend/src/components/authentication/IsAuthenticated.jsx
deleted file mode 100644
index 48322de..0000000
--- a/frontend/src/components/authentication/IsAuthenticated.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useState, useEffect } from 'react';
-
-function IsAuthenticated() {
- const [isAuthenticated, setIsAuthenticated] = useState(false);
-
- useEffect(() => {
- const access_token = localStorage.getItem('access_token');
-
- if (access_token) {
- setIsAuthenticated(true);
- } else {
- setIsAuthenticated(false);
- }
- }, []);
-
- return isAuthenticated;
-}
-
-export default IsAuthenticated;
\ No newline at end of file
diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx
index 6bba8f8..d1eedf9 100644
--- a/frontend/src/components/authentication/LoginPage.jsx
+++ b/frontend/src/components/authentication/LoginPage.jsx
@@ -5,9 +5,13 @@ import { useGoogleLogin } from "@react-oauth/google";
import refreshAccessToken from "./refreshAcesstoken";
import axiosapi from "../../api/AuthenticationApi";
+import { useAuth } from "../../hooks/authentication/IsAuthenticated";
+
function LoginPage() {
const Navigate = useNavigate();
+ const { isAuthenticated, setIsAuthenticated } = useAuth();
+
useEffect(() => {
if (!refreshAccessToken()) {
Navigate("/");
@@ -39,11 +43,13 @@ function LoginPage() {
localStorage.setItem("access_token", res.data.access);
localStorage.setItem("refresh_token", res.data.refresh);
axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access;
+ setIsAuthenticated(true);
Navigate("/");
})
.catch(err => {
console.log("Login failed");
console.log(err);
+ setIsAuthenticated(false);
});
};
@@ -58,10 +64,12 @@ function LoginPage() {
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),
diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx
index 2712f97..28eb1c2 100644
--- a/frontend/src/components/authentication/SignUpPage.jsx
+++ b/frontend/src/components/authentication/SignUpPage.jsx
@@ -1,31 +1,30 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import axiosapi from '../../api/AuthenticationApi';
-
-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';
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import axiosapi from "../../api/AuthenticationApi";
+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 © '}
+ {"Copyright © "}
TurTask
- {' '}
+ {" "}
{new Date().getFullYear()}
- {'.'}
+ {"."}
);
}
@@ -33,37 +32,36 @@ function Copyright(props) {
const defaultTheme = createTheme();
export default function SignUp() {
+ const Navigate = useNavigate();
- const Navigate = useNavigate();
+ const [formData, setFormData] = useState({
+ email: "",
+ username: "",
+ password: "",
+ });
+ const [error, setError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const [formData, setFormData] = useState({
- email: '',
- username: '',
- password: '',
- });
- const [error, setError] = useState(null);
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setIsSubmitting(true);
- setError(null);
-
- try {
- axiosapi.createUser(formData);
- } catch (error) {
- console.error('Error creating user:', error);
- setError('Registration failed. Please try again.');
- } finally {
- setIsSubmitting(false);
- }
- Navigate('/login');
- };
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData({ ...formData, [name]: value });
- };
+ const handleSubmit = async e => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ setError(null);
+
+ try {
+ axiosapi.createUser(formData);
+ } catch (error) {
+ console.error("Error creating user:", error);
+ setError("Registration failed. Please try again.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ Navigate("/login");
+ };
+
+ const handleChange = e => {
+ const { name, value } = e.target;
+ setFormData({ ...formData, [name]: value });
+ };
return (
@@ -71,13 +69,12 @@ export default function SignUp() {
-
+ marginTop: 8,
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ }}>
+
@@ -85,17 +82,17 @@ export default function SignUp() {
-
-
-
+
+
+
-
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/authentication/refreshAcessToken.jsx b/frontend/src/components/authentication/refreshAcessToken.jsx
index 89204d5..11a74fa 100644
--- a/frontend/src/components/authentication/refreshAcessToken.jsx
+++ b/frontend/src/components/authentication/refreshAcessToken.jsx
@@ -1,8 +1,8 @@
-import axios from 'axios';
+import axios from "axios";
async function refreshAccessToken() {
- const refresh_token = localStorage.getItem('refresh_token');
- const access_token = localStorage.getItem('access_token');
+ const refresh_token = localStorage.getItem("refresh_token");
+ const access_token = localStorage.getItem("access_token");
if (access_token) {
return true;
@@ -12,7 +12,7 @@ async function refreshAccessToken() {
return false;
}
- const refreshUrl = 'http://127.0.0.1:8000/api/token/refresh/';
+ const refreshUrl = "http://127.0.0.1:8000/api/token/refresh/";
try {
const response = await axios.post(refreshUrl, { refresh: refresh_token });
@@ -22,8 +22,8 @@ async function refreshAccessToken() {
const newAccessToken = response.data.access;
const newRefreshToken = response.data.refresh;
- localStorage.setItem('access_token', newAccessToken);
- localStorage.setItem('refresh_token', newRefreshToken);
+ localStorage.setItem("access_token", newAccessToken);
+ localStorage.setItem("refresh_token", newRefreshToken);
return true;
} else {
diff --git a/frontend/src/components/calendar/TaskDataHandler.jsx b/frontend/src/components/calendar/TaskDataHandler.jsx
index 3c123e9..d96c381 100644
--- a/frontend/src/components/calendar/TaskDataHandler.jsx
+++ b/frontend/src/components/calendar/TaskDataHandler.jsx
@@ -1,42 +1,26 @@
-import { fetchTodoTasks } from '../../api/TaskApi';
+import { readTodoTasks } from "../../api/TaskApi";
-let eventGuid = 0
+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,
- }));
-}
+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 [];
- }
+ try {
+ const response = await readTodoTasks();
+ return mapResponseToEvents(response);
+ } catch (error) {
+ console.error(error);
+ return [];
+ }
}
export function createEventId() {
- return String(eventGuid++);
-}
\ No newline at end of file
+ return String(eventGuid++);
+}
diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx
index 443d4bc..f1f63e5 100644
--- a/frontend/src/components/calendar/calendar.jsx
+++ b/frontend/src/components/calendar/calendar.jsx
@@ -1,11 +1,10 @@
-import React, { useState } from 'react';
+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 = {
@@ -15,9 +14,9 @@ export default class Calendar extends React.Component {
render() {
return (
-
+
{this.renderSidebar()}
-
@@ -49,23 +43,30 @@ export default class Calendar extends React.Component {
renderSidebar() {
return (
-
-
-
Instructions
-
+
+
+
Instructions
+
- Select dates and you will be prompted to create a new event
- Drag, drop, and resize events
- Click an event to delete it
-
-
+ opacity-40
+ border-2
+ border-blue-500
+ w-[350px]
+ max-h-[400px]
+ rounded-md
+ flex
+ flex-col
+ ">
);
}
@@ -64,15 +49,13 @@ function ColumnContainer({
ref={setNodeRef}
style={style}
className="
- bg-columnBackgroundColor
- w-[350px]
- h-[500px]
- max-h-[500px]
+ bg-[#f1f2f4]
+ w-[280px]
+ max-h-[400px]
rounded-md
flex
flex-col
- "
- >
+ ">
{/* Column title */}
+ ">
-
{!editMode && column.title}
{editMode && (
updateColumn(column.id, e.target.value)}
+ onChange={e => updateColumn(column.id, e.target.value)}
autoFocus
onBlur={() => {
setEditMode(false);
}}
- onKeyDown={(e) => {
+ onKeyDown={e => {
if (e.key !== "Enter") return;
setEditMode(false);
}}
@@ -137,33 +101,26 @@ function ColumnContainer({
rounded
px-1
py-2
- "
- >
-
+ ">
+
{/* Column task container */}
-
+
- {tasks.map((task) => (
-
+ {tasks.map(task => (
+
))}
{/* Column footer */}
{
createTask(column.id);
- }}
- >
-
+ }}>
+
Add task
diff --git a/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx b/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx
new file mode 100644
index 0000000..478a529
--- /dev/null
+++ b/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx
@@ -0,0 +1,19 @@
+import ColumnContainer from "./columnContainer";
+
+function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
+ return (
+
+
+
+ );
+}
+
+export default ColumnContainerCard;
diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx
index b50f354..7866bbb 100644
--- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx
+++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx
@@ -1,16 +1,10 @@
-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 ColumnContainerCard from "./columnContainerWrapper";
+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";
+import { AiOutlinePlusCircle } from "react-icons/ai";
const defaultCols = [
{
@@ -98,7 +92,7 @@ const defaultTasks = [
function KanbanBoard() {
const [columns, setColumns] = useState(defaultCols);
- const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
+ const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
const [tasks, setTasks] = useState(defaultTasks);
@@ -123,19 +117,13 @@ function KanbanBoard() {
items-center
overflow-x-auto
overflow-y-hidden
- "
- >
-
-
+ ">
+
+
- {columns.map((col) => (
- (
+ task.columnId === col.id)}
+ tasks={tasks.filter(task => task.columnId === col.id)}
/>
))}
@@ -154,48 +142,41 @@ function KanbanBoard() {
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
- "
- >
-
+ h-[60px]
+ w-[268px]
+ max-w-[268px]
+ cursor-pointer
+ rounded-xl
+ bg-[#f1f2f4]
+ border-2
+ p-4
+ hover:bg-gray-200
+ flex
+ gap-2
+ my-2
+ bg-opacity-60
+ ">
+
Add Column
{createPortal(
-
+
{activeColumn && (
- task.columnId === activeColumn.id
- )}
- />
- )}
- {activeTask && (
- task.columnId === activeColumn.id)}
/>
)}
+ {activeTask && }
,
document.body
)}
@@ -214,12 +195,12 @@ function KanbanBoard() {
}
function deleteTask(id) {
- const newTasks = tasks.filter((task) => task.id !== id);
+ const newTasks = tasks.filter(task => task.id !== id);
setTasks(newTasks);
}
function updateTask(id, content) {
- const newTasks = tasks.map((task) => {
+ const newTasks = tasks.map(task => {
if (task.id !== id) return task;
return { ...task, content };
});
@@ -237,15 +218,15 @@ function KanbanBoard() {
}
function deleteColumn(id) {
- const filteredColumns = columns.filter((col) => col.id !== id);
+ const filteredColumns = columns.filter(col => col.id !== id);
setColumns(filteredColumns);
- const newTasks = tasks.filter((t) => t.columnId !== id);
+ const newTasks = tasks.filter(t => t.columnId !== id);
setTasks(newTasks);
}
function updateColumn(id, title) {
- const newColumns = columns.map((col) => {
+ const newColumns = columns.map(col => {
if (col.id !== id) return col;
return { ...col, title };
});
@@ -280,10 +261,10 @@ function KanbanBoard() {
const isActiveAColumn = active.data.current?.type === "Column";
if (!isActiveAColumn) return;
- setColumns((columns) => {
- const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
+ setColumns(columns => {
+ const activeColumnIndex = columns.findIndex(col => col.id === activeId);
- const overColumnIndex = columns.findIndex((col) => col.id === overId);
+ const overColumnIndex = columns.findIndex(col => col.id === overId);
return arrayMove(columns, activeColumnIndex, overColumnIndex);
});
@@ -304,9 +285,9 @@ function KanbanBoard() {
if (!isActiveATask) return;
if (isActiveATask && isOverATask) {
- setTasks((tasks) => {
- const activeIndex = tasks.findIndex((t) => t.id === activeId);
- const overIndex = tasks.findIndex((t) => t.id === overId);
+ 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;
@@ -320,8 +301,8 @@ function KanbanBoard() {
const isOverAColumn = over.data.current?.type === "Column";
if (isActiveATask && isOverAColumn) {
- setTasks((tasks) => {
- const activeIndex = tasks.findIndex((t) => t.id === activeId);
+ setTasks(tasks => {
+ const activeIndex = tasks.findIndex(t => t.id === activeId);
tasks[activeIndex].columnId = overId;
return arrayMove(tasks, activeIndex, activeIndex);
diff --git a/frontend/src/components/kanbanBoard/kanbanPage.jsx b/frontend/src/components/kanbanBoard/kanbanPage.jsx
new file mode 100644
index 0000000..b362330
--- /dev/null
+++ b/frontend/src/components/kanbanBoard/kanbanPage.jsx
@@ -0,0 +1,36 @@
+import KanbanBoard from "./kanbanBoard";
+import React, { useState } from 'react';
+
+const KanbanPage = () => {
+ const [activeTab, setActiveTab] = useState('kanban');
+
+ const handleTabClick = (tabId) => {
+ setActiveTab(tabId);
+ };
+
+ return (
+
+ );
+};
+
+export default KanbanPage;
diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx
index cc2f4a4..379abf5 100644
--- a/frontend/src/components/kanbanBoard/taskCard.jsx
+++ b/frontend/src/components/kanbanBoard/taskCard.jsx
@@ -1,26 +1,18 @@
import { useState } from "react";
-import TrashIcon from "../icons/trashIcon";
+import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
+import TaskDetailModal from "./taskDetailModal";
function TaskCard({ task, deleteTask, updateTask }) {
const [mouseIsOver, setMouseIsOver] = useState(false);
- const [editMode, setEditMode] = useState(true);
- const {
- setNodeRef,
- attributes,
- listeners,
- transform,
- transition,
- isDragging,
- } = useSortable({
+ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
id: task.id,
data: {
type: "Task",
task,
},
- disabled: editMode,
});
const style = {
@@ -28,11 +20,9 @@ function TaskCard({ task, deleteTask, updateTask }) {
transform: CSS.Transform.toString(transform),
};
- const toggleEditMode = () => {
- setEditMode((prev) => !prev);
- setMouseIsOver(false);
- };
-
+ {
+ /* If card is dragged */
+ }
if (isDragging) {
return (
);
}
- if (editMode) {
- return (
+ return (
+
+
- );
- }
-
- return (
-
{
- setMouseIsOver(true);
- }}
- onMouseLeave={() => {
- setMouseIsOver(false);
- }}
- >
-
- {task.content}
-
-
- {mouseIsOver && (
-
{
- 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"
- >
-
-
- )}
);
}
diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx
new file mode 100644
index 0000000..35d5e14
--- /dev/null
+++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+function TaskDetailModal() {
+ return (
+
+ );
+}
+
+export default TaskDetailModal;
diff --git a/frontend/src/components/IconSideNav.jsx b/frontend/src/components/navigations/IconSideNav.jsx
similarity index 87%
rename from frontend/src/components/IconSideNav.jsx
rename to frontend/src/components/navigations/IconSideNav.jsx
index 982bc01..24b74fc 100644
--- a/frontend/src/components/IconSideNav.jsx
+++ b/frontend/src/components/navigations/IconSideNav.jsx
@@ -1,11 +1,6 @@
import { useState } from "react";
-import {
- AiOutlineHome,
- AiOutlineSchedule,
- AiOutlineUnorderedList,
- AiOutlinePieChart,
- AiOutlinePlus,
-} from "react-icons/ai";
+import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList, AiOutlinePieChart } from "react-icons/ai";
+import { PiStepsDuotone } from "react-icons/pi";
import { AnimatePresence, motion } from "framer-motion";
import { Link, useNavigate } from "react-router-dom";
@@ -14,7 +9,7 @@ const menuItems = [
{ id: 1, path: "/tasks", icon:
},
{ id: 2, path: "/calendar", icon:
},
{ id: 3, path: "/analytic", icon:
},
- { id: 4, path: "/priority", icon:
},
+ { id: 4, path: "/priority", icon:
},
];
const IconSideNav = () => {
@@ -29,7 +24,7 @@ const SideNav = () => {
const [selected, setSelected] = useState(0);
return (
-