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 ? ( Profile ) : ( @@ -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() { - - - + + + - @@ -148,4 +140,4 @@ 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 */}
    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 ( +
    +
    - +
    +
    + +
    +
    +
    +
    +

    Overall Statistics

    +
    +
    +
    +
    +
    +
    +
    +

    Achievements

    +
    +
    +
    +
    +
    +
    +
    +

    Friends

    +
    +
    +
    +
    +
    + + + + {/* Modal */} + +
    +
    + + + +
    +
    +
    + ); +} +export default ProfileUpdatePage; diff --git a/frontend/src/components/signup.jsx b/frontend/src/components/signup.jsx index 5c1bc7b..aba1642 100644 --- a/frontend/src/components/signup.jsx +++ b/frontend/src/components/signup.jsx @@ -1,59 +1,59 @@ -import React, { useState } from 'react'; -import axiosapi from '../api/axiosapi'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import Container from '@material-ui/core/Container'; -import Button from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; +import React, { useState } from "react"; +import axiosapi from "../api/axiosapi"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import Container from "@material-ui/core/Container"; +import Button from "@material-ui/core/Button"; +import { makeStyles } from "@material-ui/core/styles"; -const useStyles = makeStyles((theme) => ({ - // Styles for various elements - paper: { - marginTop: theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: '100%', - marginTop: theme.spacing(1), - }, - submit: { - margin: theme.spacing(3, 0, 2), - }, +const useStyles = makeStyles(theme => ({ + // Styles for various elements + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", + marginTop: theme.spacing(1), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, })); const Signup = () => { const classes = useStyles(); const [formData, setFormData] = useState({ - email: '', - username: '', - password: '', + email: "", + username: "", + password: "", }); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = async (e) => { + const handleSubmit = async e => { e.preventDefault(); setIsSubmitting(true); setError(null); try { - axiosapi.createUser(formData); + axiosapi.createUser(formData); } catch (error) { - console.error('Error creating user:', error); - setError('Registration failed. Please try again.'); // Set an error message + console.error("Error creating user:", error); + setError("Registration failed. Please try again."); // Set an error message } finally { - setIsSubmitting(false); + setIsSubmitting(false); } }; - const handleChange = (e) => { + const handleChange = e => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; @@ -102,9 +102,8 @@ const Signup = () => { variant="contained" color="primary" className={classes.submit} - disabled={isSubmitting} - > - {isSubmitting ? 'Signing up...' : 'Sign Up'} + disabled={isSubmitting}> + {isSubmitting ? "Signing up..." : "Sign Up"} {error && {error}} diff --git a/frontend/src/components/testAuth.jsx b/frontend/src/components/testAuth.jsx index abd7ba1..1edf44a 100644 --- a/frontend/src/components/testAuth.jsx +++ b/frontend/src/components/testAuth.jsx @@ -1,42 +1,47 @@ -import React, { useState, useEffect } from 'react'; -import axiosapi from '../api/AuthenticationApi'; -import { Button } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; +import React, { useState, useEffect } from "react"; +import axiosapi from "../api/AuthenticationApi"; +import { Button } from "@mui/material"; +import { useNavigate } from "react-router-dom"; function TestAuth() { - let Navigate = useNavigate(); + let Navigate = useNavigate(); - const [message, setMessage] = useState(""); + const [message, setMessage] = useState(""); - useEffect(() => { - // Fetch the "hello" data from the server when the component mounts - axiosapi.getGreeting().then(res => { - console.log(res.data); - setMessage(res.data.user); - }).catch(err => { - console.log(err); - setMessage(""); - }); - }, []); + useEffect(() => { + // Fetch the "hello" data from the server when the component mounts + axiosapi + .getGreeting() + .then(res => { + console.log(res.data); + setMessage(res.data.user); + }) + .catch(err => { + console.log(err); + setMessage(""); + }); + }, []); - const logout = () => { - // Log out the user, clear tokens, and navigate to the "/testAuth" route - axiosapi.apiUserLogout(); - Navigate('/testAuth'); - } + const logout = () => { + // Log out the user, clear tokens, and navigate to the "/testAuth" route + axiosapi.apiUserLogout(); + Navigate("/testAuth"); + }; - return ( + return ( +
    + {message !== "" && (
    - {message !== "" && ( -
    -

    Login! Hello!

    -

    {message}

    - -
    - )} - {message === "" &&

    Need to sign in, No authentication found

    } +

    Login! Hello!

    +

    {message}

    +
    - ); + )} + {message === "" &&

    Need to sign in, No authentication found

    } +
    + ); } export default TestAuth; diff --git a/frontend/src/hooks/authentication/IsAuthenticated.jsx b/frontend/src/hooks/authentication/IsAuthenticated.jsx new file mode 100644 index 0000000..8874645 --- /dev/null +++ b/frontend/src/hooks/authentication/IsAuthenticated.jsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext, useState, useEffect } from "react"; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(() => { + const access_token = localStorage.getItem("access_token"); + return !!access_token; + }); + + useEffect(() => { + const handleTokenChange = () => { + const newAccessToken = localStorage.getItem("access_token"); + setIsAuthenticated(!!newAccessToken); + }; + + handleTokenChange(); + + window.addEventListener("storage", handleTokenChange); + + return () => { + window.removeEventListener("storage", handleTokenChange); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index daa8633..fcd32b6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -8,20 +8,18 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } -.nav-link{ - color:black; +.nav-link { + color: black; /* border: 1px solid white; */ padding: 1em; -} \ No newline at end of file +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index f5430b9..38cca17 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,15 +1,20 @@ -import React from "react"; +import React, { Fragment } from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; -import { GoogleOAuthProvider} from '@react-oauth/google'; -import { BrowserRouter } from 'react-router-dom'; +import { GoogleOAuthProvider } from "@react-oauth/google"; +import { BrowserRouter } from "react-router-dom"; +import { AuthProvider } from "./hooks/authentication/IsAuthenticated"; -const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID +const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; ReactDOM.createRoot(document.getElementById("root")).render( - + + + + + -); \ No newline at end of file +); diff --git a/requirements.txt b/requirements.txt index 98f2a15..344b980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ google_auth_oauthlib>=1.1 google-auth-httplib2>=0.1 django-storages[s3]>=1.14 Pillow>=10.1 -drf-spectacular>=0.26 \ No newline at end of file +drf-spectacular>=0.26 +python-dateutil>=2.8 \ No newline at end of file