Merge pull request #87 from TurTaskProject/feature/kanban-board

Fix Kanban not work with api, fix style and now, calendar can only delete and view. Fix profile update page
This commit is contained in:
Sirin Puenggun 2023-11-28 12:04:16 +07:00 committed by GitHub
commit 601b3d0a0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1004 additions and 216 deletions

View File

@ -1,8 +1,10 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from ..models import Tag
from .serializers import TagSerializer
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
serializer_class = TagSerializer
permission_classes = (IsAuthenticated,)

View File

@ -81,6 +81,11 @@ class Todo(Task):
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
def save(self, *args, **kwargs):
done_list_name = "Done"
if self.list_board.name == done_list_name:
self.completed = True
Todo.objects.filter(list_board=self.list_board).update(completed=True)
if self.completed and not self.completion_date:
self.completion_date = timezone.now()
elif not self.completed:

View File

@ -1,9 +1,12 @@
from rest_framework import serializers
from users.models import CustomUser
from boards.models import ListBoard
from tasks.models import Todo, RecurrenceTask, Habit
from tasks.models import Todo, RecurrenceTask, Habit, Subtask
class TaskSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
sub_task_count = serializers.SerializerMethodField()
class Meta:
model = Todo
fields = '__all__'
@ -19,6 +22,12 @@ class TaskSerializer(serializers.ModelSerializer):
validated_data['user'] = user
return Todo.objects.create(**validated_data)
def get_tags(self, instance):
return [tag.name for tag in instance.tags.all()]
def get_sub_task_count(self, instance):
return instance.subtask_set.count()
class TaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
@ -97,4 +106,14 @@ class HabitTaskSerializer(serializers.ModelSerializer):
class HabitTaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Habit
exclude = ('tags',)
exclude = ('tags',)
class SubTaskSerializer(serializers.ModelSerializer):
class Meta:
model = Subtask
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
return Subtask.objects.create(**validated_data)

View File

@ -4,10 +4,13 @@ from rest_framework import viewsets, status, serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import mixins
from .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from tasks.tasks.serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer, SubTaskSerializer
from boards.models import ListBoard, KanbanTaskOrder
from tasks.models import Todo, RecurrenceTask, Habit
from tasks.models import Todo, RecurrenceTask, Habit, Subtask
from tasks.tasks.serializers import (TaskCreateSerializer,
TaskSerializer,
RecurrenceTaskSerializer,
@ -32,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet):
return TaskCreateSerializer
return TaskSerializer
def list(self, request, *args, **kwargs):
"""
list all tasks of the authenticated
user and send tags if those Todo too.
"""
try:
queryset = self.get_queryset()
serializer = TaskSerializer(queryset, many=True)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def create(self, request, *args, **kwargs):
try:
new_task_data = request.data
@ -117,6 +132,73 @@ class TodoViewSet(viewsets.ModelViewSet):
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(name='parent_task', description='Parent Task ID', type=int),
]
)
)
class SubTaskViewset(viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
mixins.UpdateModelMixin):
queryset = Subtask.objects.all()
permission_classes = (IsAuthenticated,)
def get_serializer_class(self):
return SubTaskSerializer
def list(self, request, *args, **kwargs):
"""List only subtask of parent task."""
try:
parent_task = request.query_params.get('parent_task')
if not parent_task:
raise serializers.ValidationError('parent_task is required.')
queryset = self.get_queryset().filter(parent_task_id=parent_task)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def create(self, request, *args, **kwargs):
"""Create a new subtask, point to some parent tasks."""
try:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def destroy(self, request, *args, **kwargs):
"""Delete a subtask."""
try:
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def partial_update(self, request, *args, **kwargs):
"""Update a subtask."""
try:
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class RecurrenceTaskViewSet(viewsets.ModelViewSet):
queryset = RecurrenceTask.objects.all()
serializer_class = RecurrenceTaskSerializer

View File

@ -0,0 +1,33 @@
from datetime import datetime, timedelta
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tasks.tests.utils import create_test_user
from tasks.models import Todo
from boards.models import ListBoard, Board
class TodoSignalHandlersTests(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
self.list_board = Board.objects.get(user=self.user).listboard_set.first()
def test_update_priority_signal_handler(self):
"""
Test the behavior of the update_priority signal handler.
"""
due_date = datetime.now() + timedelta(days=5)
data = {
'title': 'Test Task',
'type': 'habit',
'difficulty': 1,
'end_event': due_date.strftime('%Y-%m-%dT%H:%M:%S'),
'list_board': self.list_board.id,
}
response = self.client.post(reverse("todo-list"), data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# Retrieve the created task and check if priority is updated
task = Todo.objects.get(title='Test Task')
self.assertIsNotNone(task.priority) # Check if priority is not None

View File

@ -3,7 +3,7 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter
from tasks.api import GoogleCalendarEventViewset
from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet
from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet, SubTaskViewset
from tasks.misc.views import TagViewSet
@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet)
router.register(r'habit', HabitTaskViewSet)
router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
router.register(r'subtasks', SubTaskViewset, basename='subtasks')
urlpatterns = [
path('', include(router.urls)),

View File

@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
Serializer for updating user profile.
"""
profile_pic = serializers.ImageField(required=False)
first_name = serializers.CharField(max_length=255, required=False)
username = serializers.CharField(max_length=255, required=False)
about = serializers.CharField(required=False)
class Meta:
model = CustomUser
fields = ('profile_pic', 'first_name', 'about')
fields = ('profile_pic', 'username', 'about')
def update(self, instance, validated_data):
"""
Update an existing user's profile.
"""
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
class UpdateProfileNopicSerializer(serializers.ModelSerializer):
"""
Serializer for updating user profile.
"""
username = serializers.CharField(max_length=255, required=False)
about = serializers.CharField(required=False)
class Meta:
model = CustomUser
fields = ('username', 'about')
def update(self, instance, validated_data):
"""

View File

@ -9,7 +9,7 @@ from rest_framework.parsers import MultiPartParser
from rest_framework_simplejwt.tokens import RefreshToken
from users.serializers import CustomUserSerializer, UpdateProfileSerializer
from users.serializers import CustomUserSerializer, UpdateProfileSerializer, UpdateProfileNopicSerializer
from users.models import CustomUser
class CustomUserCreate(APIView):
@ -57,13 +57,17 @@ class CustomUserProfileUpdate(APIView):
return Response ({
'error': 'User does not exist'
}, status=status.HTTP_404_NOT_FOUND)
serializer = UpdateProfileSerializer(request.user, data=request.data)
if request.data.get('profile_pic') == "null":
serializer = UpdateProfileNopicSerializer(request.user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin):
queryset = CustomUser.objects.all()
permission_classes = (IsAuthenticated,)
@ -72,4 +76,4 @@ class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin)
def retrieve(self, request, *args, **kwargs):
serializer = self.get_serializer(request.user)
return Response(serializer.data)

View File

@ -34,6 +34,7 @@
"@wojtekmaj/react-daterange-picker": "^5.4.4",
"axios": "^1.6.1",
"bootstrap": "^5.3.2",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"framer-motion": "^10.16.4",
"gapi-script": "^1.2.0",
@ -42,9 +43,11 @@
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-bootstrap": "^2.9.1",
"react-datepicker": "^4.23.0",
"react-datetime-picker": "^5.5.3",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
"react-ios-time-picker": "^0.2.2",
"react-router-dom": "^6.18.0",
"react-tsparticles": "^2.12.2",
"tsparticles": "^2.12.0"

View File

@ -77,6 +77,9 @@ dependencies:
bootstrap:
specifier: ^5.3.2
version: 5.3.2(@popperjs/core@2.11.8)
date-fns:
specifier: ^2.30.0
version: 2.30.0
dotenv:
specifier: ^16.3.1
version: 16.3.1
@ -101,6 +104,9 @@ dependencies:
react-bootstrap:
specifier: ^2.9.1
version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
react-datepicker:
specifier: ^4.23.0
version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
react-datetime-picker:
specifier: ^5.5.3
version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
@ -110,6 +116,9 @@ dependencies:
react-icons:
specifier: ^4.11.0
version: 4.12.0(react@18.2.0)
react-ios-time-picker:
specifier: ^0.2.2
version: 0.2.2(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: ^6.18.0
version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
@ -3473,6 +3482,22 @@ packages:
- '@types/react-dom'
dev: false
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
peerDependencies:
react: ^16.9.0 || ^17 || ^18
react-dom: ^16.9.0 || ^17 || ^18
dependencies:
'@popperjs/core': 2.11.8
classnames: 2.3.2
date-fns: 2.30.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0)
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
dev: false
/react-datetime-picker@5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==}
peerDependencies:
@ -3520,6 +3545,10 @@ packages:
scheduler: 0.23.0
dev: false
/react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
/react-fit@1.7.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==}
peerDependencies:
@ -3550,6 +3579,17 @@ packages:
react: 18.2.0
dev: false
/react-ios-time-picker@0.2.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-bi+K23lK6Pf2xDXmhAlz+RJuy9/onWYi7Ye+ODVhIkis9AVFECOza2ckkZl/4vUypj2+TdTsHn+VZrTNdGIwDQ==}
peerDependencies:
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-portal: 4.2.2(react-dom@18.2.0)(react@18.2.0)
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -3565,6 +3605,41 @@ packages:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
/react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==}
peerDependencies:
react: ^15.5.x || ^16.x || ^17.x || ^18.x
react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
peerDependencies:
'@popperjs/core': ^2.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
dependencies:
'@popperjs/core': 2.11.8
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-fast-compare: 3.2.2
warning: 4.0.3
dev: false
/react-portal@4.2.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==}
peerDependencies:
react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0
react-dom: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0
dependencies:
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies:

View File

@ -0,0 +1,44 @@
import { axiosInstance } from "./AxiosConfig";
export const getSubtask = async (parentTaskId) => {
try {
const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`);
return response.data;
} catch (error) {
console.error("Error fetching subtasks:", error);
throw error;
}
};
export const addSubtasks = async (parentTaskId, text) => {
try {
const response = await axiosInstance.post("subtasks/", {
description: text,
completed: false,
parent_task: parentTaskId,
});
return response.data;
} catch (error) {
console.error("Error adding subtask:", error);
throw error;
}
};
export const deleteSubtasks = async (subtaskId) => {
try {
await axiosInstance.delete(`subtasks/${subtaskId}/`);
} catch (error) {
console.error("Error deleting subtask:", error);
throw error;
}
};
export const updateSubtask = async (subtaskId, data) => {
try {
const response = await axiosInstance.patch(`subtasks/${subtaskId}/`, data);
return response.data;
} catch (error) {
console.error("Error updating subtask:", error);
throw error;
}
};

View File

@ -38,6 +38,15 @@ export const updateTask = (endpoint, id, data) => {
});
};
export const updateTaskPartial = (endpoint, id, data) => {
return axiosInstance
.patch(`${baseURL}${endpoint}/${id}/`, data)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
export const deleteTask = (endpoint, id) => {
return axiosInstance
.delete(`${baseURL}${endpoint}/${id}/`)
@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id);
// Update
export const updateTodoTask = (id, data) => updateTask("todo", id, data);
export const updateTodoTaskPartial = (id, data) => updateTaskPartial("todo", id, data);
export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
export const updateHabitTask = (id, data) => updateTask("habit", id, data);

View File

@ -11,8 +11,6 @@ const ApiUpdateUserProfile = async (formData) => {
},
});
console.log(response.data);
return response.data;
} catch (error) {
console.error("Error updating user profile:", error);

View File

@ -1,5 +1,10 @@
import { useState, useEffect } from "react";
import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi";
import {
FiAlertCircle,
FiClock,
FiXCircle,
FiCheckCircle,
} from "react-icons/fi";
import { readTodoTasks } from "../../api/TaskApi";
import { axiosInstance } from "src/api/AxiosConfig";
@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) {
};
return (
<div className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}>
<div
className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}
>
<div className="flex" style={{ color: colorCode }}>
<span className="mx-2 mt-1">{icon}</span>
<span>{name}</span>
@ -39,10 +46,14 @@ function EachBlog({ name, colorCode, contentList, icon }) {
<input
type="checkbox"
checked={item.completed}
className="checkbox mt-1 mr-2"
className="checkbox mt-1 mr-2 bg-gray-300 "
onChange={() => handleCheckboxChange(index)}
/>
<label className={`cursor-pointer ${item.completed ? "line-through text-gray-500" : ""}`}>
<label
className={`cursor-pointer ${
item.completed ? "line-through text-gray-500" : ""
}`}
>
{item.title}
</label>
</div>

View File

@ -5,6 +5,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { getEvents, createEventId } from "./TaskDataHandler";
import { axiosInstance } from "src/api/AxiosConfig";
export class Calendar extends React.Component {
state = {
@ -25,13 +26,13 @@ export class Calendar extends React.Component {
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
initialView="dayGridMonth"
editable={true}
selectable={true}
editable={false}
selectable={false}
selectMirror={true}
dayMaxEvents={true}
weekends={this.state.weekendsVisible}
initialEvents={getEvents}
select={this.handleDateSelect}
// select={this.handleDateSelect}
eventContent={renderEventContent}
eventClick={this.handleEventClick}
eventsSet={this.handleEvents}
@ -85,22 +86,22 @@ export class Calendar extends React.Component {
});
};
handleDateSelect = (selectInfo) => {
let title = prompt("Please enter a new title for your event");
let calendarApi = selectInfo.view.calendar;
// handleDateSelect = (selectInfo) => {
// let title = prompt("Please enter a new title for your event");
// let calendarApi = selectInfo.view.calendar;
calendarApi.unselect(); // clear date selection
// calendarApi.unselect(); // clear date selection
if (title) {
calendarApi.addEvent({
id: createEventId(),
title,
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay,
});
}
};
// if (title) {
// calendarApi.addEvent({
// id: createEventId(),
// title,
// start: selectInfo.startStr,
// end: selectInfo.endStr,
// allDay: selectInfo.allDay,
// });
// }
// };
handleEventClick = (clickInfo) => {
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {

View File

@ -28,6 +28,7 @@ export function Dashboard() {
const [totalTask, setTotalTask] = useState(0);
const [totalCompletedTasks, settotalCompletedTasks] = useState(0);
const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0);
const [totalTaskToday, setTotalTaskToday] = useState(0);
const [progressData, setProgressData] = useState(0);
const [overdueTask, setOverdueTask] = useState(0);
@ -36,19 +37,16 @@ export function Dashboard() {
const response = await axiosInstance.get("/dashboard/todostats/");
const totalTaskValue = response.data.total_tasks || 0;
const totalCompletedTasksValue = response.data.total_completed_tasks || 0;
const totalTaskTodayValue = response.data.total_task_today || 0;
const totalCompletedTasksTodayValue =
response.data.total_completed_tasks_today || 0;
const totalTaskToday = response.data.total_task_today || 0;
const totalCompletedTasksToday = response.data.tasks_completed_today || 0;
response.data.tasks_completed_today || 0;
const overdueTasks = response.data.overdue_tasks || 0;
const progress =
(totalCompletedTasksToday / totalCompletedTasksToday) * 100;
const progress = (totalCompletedTasksToday / totalTaskToday) * 100;
setTotalTask(totalTaskValue);
settotalCompletedTasks(totalCompletedTasksValue);
setTotalCompletedTasksToday(totalCompletedTasksTodayValue);
setTotalTaskToday(totalTaskToday);
setTotalTaskToday(totalTaskTodayValue);
setProgressData(progress);
setOverdueTask(overdueTasks);
};
@ -147,7 +145,11 @@ export function Dashboard() {
<Flex className="flex-col items-center">
<ProgressCircle
className="mt-6"
value={progressData}
value={
isNaN(progressData) || !isFinite(progressData)
? 0
: `${progressData.toFixed(0)}%`
}
size={200}
strokeWidth={10}
radius={60}

View File

@ -9,8 +9,17 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT
return tasks.map((task) => task.id);
}, [tasks]);
const { setNodeRef, attributes, listeners } = useSortable({
id: column.id,
data: {
type: "Column",
column,
},
});
return (
<div
ref={setNodeRef}
className="
bg-[#f1f2f4]
w-[280px]
@ -21,6 +30,8 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT
">
{/* Column title */}
<div
{...attributes}
{...listeners}
className="
ml-3
text-md

View File

@ -1,12 +1,6 @@
import { useMemo, useState, useEffect } from "react";
import { ColumnContainerCard } from "./columnContainerWrapper";
import {
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
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";
@ -32,9 +26,7 @@ export function KanbanBoard() {
// ---------------- Task Handlers ----------------
const handleTaskUpdate = (tasks, updatedTask) => {
const updatedTasks = tasks.map((task) =>
task.id === updatedTask.id ? updatedTask : task
);
const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task));
setTasks(updatedTasks);
};
@ -129,6 +121,7 @@ export function KanbanBoard() {
user: task.user,
list_board: task.list_board,
tags: task.tags,
subtaskCount: task.sub_task_count,
}));
setTasks(transformedTasks);
@ -176,14 +169,8 @@ export function KanbanBoard() {
justify-center
overflow-x-auto
overflow-y-hidden
"
>
<DndContext
sensors={sensors}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
>
">
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
<div className="flex gap-4">
<div className="flex gap-4">
{!isLoading ? (
@ -195,9 +182,7 @@ export function KanbanBoard() {
createTask={createTask}
deleteTask={deleteTask}
updateTask={updateTask}
tasks={(tasks || []).filter(
(task) => task.columnId === col.id
)}
tasks={(tasks || []).filter((task) => task.columnId === col.id)}
/>
))}{" "}
</SortableContext>
@ -210,11 +195,7 @@ export function KanbanBoard() {
{createPortal(
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
{/* Render the active task as a draggable overlay */}
<TaskCard
task={activeTask}
deleteTask={deleteTask}
updateTask={updateTask}
/>
<TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />
</DragOverlay>,
document.body
)}
@ -240,26 +221,43 @@ export function KanbanBoard() {
if (!over) return; // If not dropped over anything, exit
const activeId = active.id;
const overId = over.id;
const isActiveATask = active.data.current?.type === "Task";
const isOverATask = over.data.current?.type === "Task";
const isOverAColumn = over.data.current?.type === "Column";
if (isActiveATask && isOverATask) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
const columnId = over.data.current.task.columnId;
tasks[activeIndex].columnId = columnId;
// API call to update task's columnId
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
return arrayMove(tasks, activeIndex, activeIndex);
});
}
// Move tasks between columns and update columnId
if (isActiveATask && isOverAColumn) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
// Extract the column ID from overId
const columnId = extractColumnId(overId);
const columnId = over.data.current.column.id;
tasks[activeIndex].columnId = columnId;
// API call to update task's columnId
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_list_board_id: columnId,
new_index: 0,
})
.then((response) => {})
@ -271,15 +269,6 @@ export function KanbanBoard() {
});
}
}
// Helper function to extract the column ID from the element ID
function extractColumnId(elementId) {
// Implement logic to extract the column ID from elementId
// For example, if elementId is in the format "column-123", you can do:
const parts = elementId.split("-");
return parts.length === 2 ? parseInt(parts[1], 10) : null;
}
// Handle the drag-over event
function onDragOver(event) {
const { active, over } = event;
@ -306,39 +295,15 @@ export function KanbanBoard() {
tasks[activeIndex].columnId = tasks[overIndex].columnId;
return arrayMove(tasks, activeIndex, overIndex - 1);
}
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
return arrayMove(tasks, activeIndex, overIndex);
});
}
const isOverAColumn = over.data.current?.type === "Column";
// Move the Task to a different column and update columnId
if (
isActiveATask &&
isOverAColumn &&
tasks.some((task) => task.columnId !== overId)
) {
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
tasks[activeIndex].columnId = overId;
return arrayMove(tasks, activeIndex, activeIndex);
});

View File

@ -1,16 +1,14 @@
import { useState } from "react";
import { useEffect } from "react";
import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { TaskDetailModal } from "./taskDetailModal";
import { GoChecklist, GoArchive } from "react-icons/go";
export function TaskCard({ task, deleteTask, updateTask }) {
// State to track if the mouse is over the task card
const [mouseIsOver, setMouseIsOver] = useState(false);
// console.log(task.challenge);
// console.log(task.importance);
// console.log(task.difficulty);
// DnD Kit hook for sortable items
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
id: task.id,
data: {
@ -18,68 +16,163 @@ export function TaskCard({ task, deleteTask, updateTask }) {
task,
},
});
// Style for the task card, adjusting for dragging animation
const style = {
transition,
transform: CSS.Transform.toString(transform),
};
// ---- DESC AND TAG ---- */
{
/* If card is dragged */
if (task.tags === undefined) {
task.tags = [];
}
// Tags
const tags =
task.tags.length > 0 ? (
<div className="flex flex-wrap mx-3 mt-4">
{task.tags.map((tag, index) => (
<div
key={index}
className={`inline-flex items-center font-bold leading-sm uppercase w-1/3 h-3 p-2 mr-1 bg-${tag.color}-200 text-${tag.color}-700 rounded`}>
<p className="text-[9px] truncate">{tag}</p>
</div>
))}
</div>
) : null;
// difficulty?
const difficultyTag = task.difficulty ? (
<span
className={`text-[9px] inline-flex items-center font-bold leading-sm uppercase px-2 py-1 rounded-full ${
task.difficulty === 1
? "bg-blue-200 text-blue-700"
: task.difficulty === 2
? "bg-green-200 text-green-700"
: task.difficulty === 3
? "bg-yellow-200 text-yellow-700"
: task.difficulty === 4
? "bg-red-200 text-red-700"
: "bg-purple-200 text-purple-700"
}`}>
difficulty
</span>
) : null;
// Due Date
const dueDateTag =
task.end_event && new Date(task.end_event) > new Date()
? (() => {
const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24));
let colorClass =
daysUntilDue >= 365
? "gray-200"
: daysUntilDue >= 30
? "blue-200"
: daysUntilDue >= 7
? "green-200"
: daysUntilDue > 0
? "yellow-200"
: "red-200";
const formattedDueDate =
daysUntilDue >= 365
? new Date(task.end_event).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
})
: new Date(task.end_event).toLocaleDateString("en-US", { day: "numeric", month: "short" });
return (
<span className={`bg-${colorClass} text-[10px] font-xl font-bold px-2 py-1 rounded-full`}>
Due: {formattedDueDate}
</span>
);
})()
: null;
// Subtask count
const subtaskCountTag = task.subtaskCount ? (
<span className="flex flex-row items-center bg-green-200 text-green-600 text-[10px] font-xl font-bold me-2 px-2.5 py-0.5 rounded">
<GoChecklist /> {task.subtaskCount}
</span>
) : null;
// ---- DRAG STATE ---- */
// If the card is being dragged
if (isDragging) {
return (
<div
ref={setNodeRef}
style={style}
className="
opacity-30
bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative
"
className="opacity-30 bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative"
/>
);
}
// If the card is not being dragged
return (
<div>
{/* Task Detail Modal */}
<TaskDetailModal
taskId={task.id}
title={task.content}
description={task.description}
tags={task.tags}
difficulty={task.difficulty}
f challenge={task.challenge}
challenge={task.challenge}
importance={task.importance}
updateTask={updateTask}
completed={task.completed}
/>
{/* -------- Task Card -------- */}
<div
ref={setNodeRef}
{...attributes}
{...listeners}
style={style}
className="justify-center items-center flex text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
className="justify-center flex flex-col text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
onMouseEnter={() => {
setMouseIsOver(true);
}}
onMouseLeave={() => {
setMouseIsOver(false);
}}>
<p
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white`}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{task.content}
</p>
{mouseIsOver && (
<button
onClick={() => {
deleteTask(task.id);
}}
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 ">
<BsFillTrashFill />
</button>
)}
}}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{/* -------- Task Content -------- */}
{/* Tags */}
{tags}
<div>
{/* Title */}
<p
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl bg-white font-semibold`}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{task.content}
</p>
{/* -------- Archive Task Button -------- */}
{mouseIsOver && (
<button
onClick={() => {
deleteTask(task.id);
}}
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 ">
<GoArchive />
</button>
)}
</div>
{/* Description */}
<div className="flex flex-wrap mb-4 mx-3 space-x-1">
{difficultyTag}
{dueDateTag}
{subtaskCountTag}
</div>
</div>
</div>
);
}
}

View File

@ -1,44 +1,283 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6";
import { FaPlus, FaRegTrashCan, FaPencil } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { addSubtasks, deleteSubtasks, getSubtask, updateSubtask } from "src/api/SubTaskApi";
import { updateTodoTaskPartial } from "src/api/TaskApi";
import format from "date-fns/format";
export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) {
export function TaskDetailModal({
title,
description,
tags,
difficulty,
challenge,
importance,
taskId,
updateTask,
completed,
}) {
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
// console.log(currentDifficulty);
// console.log(isChallengeChecked);
// console.log(isImportantChecked);
const [currentDifficulty, setCurrentDifficulty] = useState((difficulty - 1) * 25);
const [selectedTags, setSelectedTags] = useState([]);
const [dateStart, setDateStart] = useState(new Date());
const [dateEnd, setDateEnd] = useState(new Date());
const [startDateEnabled, setStartDateEnabled] = useState(false);
const [endDateEnabled, setEndDateEnabled] = useState(false);
const [isTaskComplete, setTaskComplete] = useState(completed);
const [starteventValue, setStartEventValue] = useState("10:00 PM");
const [endeventValue, setEndEventValue] = useState("11:00 AM");
const [subtaskText, setSubtaskText] = useState("");
const [subtasks, setSubtasks] = useState([]);
const [currentTitle, setTitle] = useState(title);
const [isTitleEditing, setTitleEditing] = useState(false);
const handleChallengeChange = () => {
const handleTitleChange = async () => {
const data = {
title: currentTitle,
};
await updateTodoTaskPartial(taskId, data);
setTitleEditing(false);
};
const handleStartEventTimeChange = async (timeValue) => {
const formattedTime = convertToFormattedTime(timeValue);
setStartEventValue(formattedTime);
console.log(formattedTime);
const data = {
startTime: formattedTime,
};
await updateTodoTaskPartial(taskId, data);
};
const handleEndEventTimeChange = async (timeValue) => {
const inputTime = event.target.value;
// Validate the input time format
if (!validateTimeFormat(inputTime)) {
// Display an error message or handle invalid format
console.error("Invalid time format. Please use HH:mm AM/PM");
return;
}
const formattedTime = convertToFormattedTime(timeValue);
setEndEventValue(formattedTime);
const data = {
endTime: formattedTime,
};
await updateTodoTaskPartial(taskId, data);
};
const convertToFormattedTime = (timeValue) => {
const formattedTime = format(timeValue, "HH:mm:ss.SSSX", { timeZone: "UTC" });
return formattedTime;
};
const validateTimeFormat = (time) => {
const timeFormatRegex = /^(0[1-9]|1[0-2]):[0-5][0-9] (AM|PM)$/i;
return timeFormatRegex.test(time);
};
const handleChallengeChange = async () => {
setChallengeChecked(!isChallengeChecked);
const data = {
challenge: !isChallengeChecked,
};
await updateTodoTaskPartial(taskId, data);
};
const handleImportantChange = () => {
const handleImportantChange = async () => {
setImportantChecked(!isImportantChecked);
const data = {
important: !isImportantChecked,
};
await updateTodoTaskPartial(taskId, data);
};
const handleDifficultyChange = (event) => {
const handleDifficultyChange = async (event) => {
setCurrentDifficulty(parseInt(event.target.value, 10));
let diff = event.target.value / 25 + 1;
const data = {
difficulty: diff,
};
await updateTodoTaskPartial(taskId, data);
};
const handleTagChange = (tag) => {
const isSelected = selectedTags.includes(tag);
setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]);
``;
};
const handleStartDateValueChange = (date) => {
if (!isTaskComplete) {
setDateStart(date);
const formattedStartDate = convertToFormattedDate(date);
const data = {
startTime: formattedStartDate,
};
updateTodoTaskPartial(taskId, data);
}
};
const handleEndDateValueChange = (date) => {
if (!isTaskComplete) {
setDateEnd(date);
const formattedEndDate = convertToFormattedDate(date);
const data = {
endTime: formattedEndDate,
};
updateTodoTaskPartial(taskId, data);
}
};
const convertToFormattedDate = (dateValue) => {
const formattedDate = format(dateValue, "yyyy-MM-dd'T'", { timeZone: "UTC" });
return formattedDate;
};
const handleStartDateChange = () => {
if (!isTaskComplete) {
setStartDateEnabled(!startDateEnabled);
}
};
const handleEndDateChange = () => {
if (!isTaskComplete) {
setEndDateEnabled(!endDateEnabled);
}
};
const handleTaskCompleteChange = async () => {
let completed = false;
if (isTaskComplete) {
setTaskComplete(false);
completed = false;
} else {
setTaskComplete(true);
completed = true;
setStartDateEnabled(false);
setEndDateEnabled(false);
}
const data = {
completed: completed,
};
await updateTodoTaskPartial(taskId, data);
};
const addSubtask = async () => {
try {
if (subtaskText.trim() !== "") {
const newSubtask = await addSubtasks(taskId, subtaskText.trim());
setSubtasks([...subtasks, newSubtask]);
setSubtaskText("");
}
} catch (error) {
console.error("Error adding subtask:", error);
}
};
const toggleSubtaskCompletion = async (index) => {
try {
const updatedSubtasks = [...subtasks];
updatedSubtasks[index].completed = !updatedSubtasks[index].completed;
await updateSubtask(updatedSubtasks[index].id, { completed: updatedSubtasks[index].completed });
setSubtasks(updatedSubtasks);
} catch (error) {
console.error("Error updating subtask:", error);
}
};
const deleteSubtask = async (index) => {
try {
await deleteSubtasks(subtasks[index].id);
const updatedSubtasks = [...subtasks];
updatedSubtasks.splice(index, 1);
setSubtasks(updatedSubtasks);
} catch (error) {
console.error("Error deleting subtask:", error);
}
};
const subtaskElements = subtasks.map((subtask, index) => (
<div key={index} className="flex items-center space-x-2">
<input
type="checkbox"
checked={subtask.completed}
className="checkbox checkbox-xs bg-gray-400"
onChange={() => toggleSubtaskCompletion(index)}
/>
<div className={`flex items-center rounded p-2 shadow border-2 ${subtask.completed && "line-through"}`}>
{subtask.description}
<FaRegTrashCan className="cursor-pointer ml-2 text-red-500" onClick={() => deleteSubtask(index)} />
</div>
</div>
));
useEffect(() => {
const fetchSubtasks = async () => {
try {
const fetchedSubtasks = await getSubtask(taskId);
setSubtasks(fetchedSubtasks);
} catch (error) {
console.error("Error fetching subtasks:", error);
}
};
fetchSubtasks();
}, [taskId]);
// Existing tags
const existingTags = tags.map((tag, index) => (
<div
key={index}
className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}>
{tag.name}
</div>
));
// Selected tags
const selectedTagElements = selectedTags.map((tag, index) => (
<div
key={index}
className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}>
{tag.name}
</div>
));
return (
<dialog id={`task_detail_modal_${taskId}`} className="modal">
<div className="modal-box w-4/5 max-w-3xl">
{/* Title */}
<div className="flex flex-col py-2">
<div className="flex flex-col">
<h3 className="font-bold text-lg">
<span className="flex gap-2">
{<FaTasks className="my-2" />}
{title}
</span>
</h3>
<p className="text-xs">{title}</p>
{isTitleEditing ? (
<div className="flex gap-2 items-center">
<FaTasks className="my-2" />
<input
type="text"
className="input-md input-bordered font-bold text-lg"
value={currentTitle}
onChange={(e) => setTitle(e.target.value)}
/>
<button className="btn btn-sm" onClick={handleTitleChange}>
Save
</button>
</div>
) : (
<h3 className="font-bold text-lg">
<span className="flex gap-2">
{<FaTasks className="my-2" />}
{currentTitle}
<FaPencil className="my-2" onClick={() => setTitleEditing(true)} />
</span>
</h3>
)}
<p className="text-xs">{currentTitle}</p>
</div>
</div>
{/* Tags */}
<div className="flex flex-col py-2 pb-4">
<div className="flex flex-row space-x-5">
@ -46,19 +285,89 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
+ Add Tags
</label>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2
</a>
</li>
<ul tabIndex={0} className="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box w-52">
{tags.map((tag, index) => (
<li key={index}>
<label className="cursor-pointer space-x-2">
<input
type="checkbox"
checked={selectedTags.includes(tag)}
className="checkbox checkbox-sm"
onChange={() => handleTagChange(tag)}
/>
{tag}
</label>
</li>
))}
</ul>
</div>
</div>
<div className="flex flex-nowrap overflow-x-auto"></div>
<div className="flex flex-nowrap overflow-x-auto">
{existingTags}
{selectedTagElements}
</div>
</div>
{/* Date Picker */}
<div className="flex flex-col space-y-2 mb-2">
{/* Start */}
<div className="flex flex-row items-center">
<div>
<p className="text-xs font-bold">Start At</p>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={startDateEnabled}
className="checkbox checkbox-xs bg-gray-400"
onChange={handleStartDateChange}
/>
<div className={`rounded p-2 shadow border-2 ${!startDateEnabled && "opacity-50"}`}>
<DatePicker selected={dateStart} onChange={handleStartDateValueChange} disabled={!startDateEnabled} />
</div>
</div>
</div>
{/* Start event time picker */}
<div className="rounded p-2 shadow border-2 ml-2 mt-4">
{/* handleStartEventTimeChange */}
<input
type="text"
placeholder="10:00 AM"
className="input input-bordered w-full max-w-xs"
onClick={handleStartEventTimeChange}
/>
</div>
{/* Complete? */}
<div className="mx-4">
<div className="flex items-center space-x-2 mt-4">
<div className="flex-1 flex-row card shadow border-2 p-2 pr-2">
<p className="text-md mx-2">Complete</p>
<input type="checkbox" checked={isTaskComplete} className="checkbox checkbox-xl bg-gray-400" />
<button className="btn btn-sm mt-2" onClick={handleStartEventTimeChange}>
Update Start Time
</button>
</div>
</div>
</div>
</div>
{/* End */}
<div>
<p className="text-xs font-bold">End At</p>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={endDateEnabled}
className="checkbox checkbox-xs bg-gray-400"
onChange={handleEndDateChange}
/>
<div className={`rounded p-2 shadow border-2 ${!endDateEnabled && "opacity-50"}`}>
<DatePicker selected={dateEnd} onChange={handleEndDateValueChange} disabled={!endDateEnabled} />
</div>
{/* End event time picker */}
<div className="rounded p-2 shadow border-2">this is time picker</div>
</div>
</div>
</div>
{/* Description */}
<div className="flex flex-col gap-2">
<h2 className="font-bold">
@ -71,7 +380,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
{description}
</textarea>
</div>
{/* Difficulty, Challenge, and Importance */}
<div className="flex flex-row space-x-3 my-4">
<div className="flex-1 card shadow border-2 p-2">
@ -101,7 +409,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<input
type="checkbox"
checked={isChallengeChecked}
className="checkbox"
className="checkbox bg-black"
onChange={handleChallengeChange}
/>
</label>
@ -116,14 +424,13 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<input
type="checkbox"
checked={isImportantChecked}
className="checkbox"
className="checkbox bg-black"
onChange={handleImportantChange}
/>
</label>
</div>
</div>
</div>
{/* Subtask */}
<div className="flex flex-col pt-2">
<h2 className="font-bold">
@ -133,14 +440,21 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
</span>
</h2>
<div className="flex space-x-3 pt-2">
<input type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" />
<button className="btn">
<input
type="text"
placeholder="subtask topic"
className="input input-bordered flex-1 w-full"
value={subtaskText}
onChange={(e) => setSubtaskText(e.target.value)}
/>
<button className="btn" onClick={addSubtask}>
<FaPlus />
Add Subtask
</button>
</div>
{/* Display Subtasks */}
<div className="flex flex-col space-y-2 pt-2">{subtaskElements}</div>
</div>
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
</form>

View File

@ -1,6 +1,8 @@
import { useNavigate } from "react-router-dom";
import { apiUserLogout } from "src/api/AuthenticationApi";
import { useAuth } from "src/hooks/AuthHooks";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect, useState } from "react";
const settings = {
Profile: "/profile",
@ -10,6 +12,7 @@ const settings = {
export function NavBar() {
const Navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth();
const [profile_pic, setProfilePic] = useState(undefined);
const logout = () => {
apiUserLogout();
@ -17,6 +20,25 @@ export function NavBar() {
Navigate("/");
};
useEffect(() => {
const fetchUser = async () => {
if (isAuthenticated) {
try {
const response = await axiosInstance.get("/user/data/");
const fetchedProfilePic = response.data.profile_pic;
setProfilePic(fetchedProfilePic);
} catch (error) {
console.error("Error fetching user:", error);
}
} else {
setProfilePic(
"https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png"
);
}
};
fetchUser();
}, []);
return (
<div data-theme="night" className="navbar bg-base-100">
<div className="flex-1">
@ -32,19 +54,23 @@ export function NavBar() {
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png" />
<img src={profile_pic} />
</div>
</label>
<ul
tabIndex={0}
className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52"
>
<li>
<a href={settings.Profile} className="justify-between">
<a
onClick={() => Navigate(settings.Profile)}
className="justify-between"
>
Profile
</a>
</li>
<li>
<a href={settings.Account}>Settings</a>
<a onClick={() => Navigate(settings.Account)}>Settings</a>
</li>
<li>
<a onClick={logout}>Logout</a>
@ -53,10 +79,16 @@ export function NavBar() {
</div>
) : (
<div className="flex gap-2">
<button className="btn btn-outline btn-info" onClick={() => Navigate("/login")}>
<button
className="btn btn-outline btn-info"
onClick={() => Navigate("/login")}
>
Login
</button>
<button className="btn btn-success" onClick={() => Navigate("/signup")}>
<button
className="btn btn-success"
onClick={() => Navigate("/signup")}
>
Sign Up
</button>
</div>

View File

@ -1,13 +1,30 @@
import { useState, useRef } from "react";
import { ApiUpdateUserProfile } from "src/api/UserProfileApi";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect } from "react";
export 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 [about, setAbout] = useState();
const fileInputRef = useRef(null);
const [profile_pic, setProfilePic] = useState(undefined);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axiosInstance.get("/user/data/");
const fetchedProfilePic = response.data.profile_pic;
const fetchedName = response.data.username;
const fetchedAbout = response.data.about;
setProfilePic(fetchedProfilePic);
setAbout(fetchedAbout);
setUserName(fetchedName);
} catch (error) {
console.error("Error fetching user:", error);
}
};
fetchUser();
}, []);
const handleImageUpload = () => {
if (fileInputRef.current) {
@ -25,7 +42,7 @@ export function ProfileUpdateComponent() {
const handleSave = () => {
const formData = new FormData();
formData.append("profile_pic", file);
formData.append("first_name", username);
formData.append("username", username);
formData.append("about", about);
ApiUpdateUserProfile(formData);
@ -50,7 +67,7 @@ export function ProfileUpdateComponent() {
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
) : (
<>
<img src={defaultImage} alt="Default" className="rounded-full" />
<img src={profile_pic} alt="Default" className="rounded-full" />
<i className="fas fa-camera text-white text-2xl absolute bottom-0 right-0 mr-2 mb-2"></i>
<i className="fas fa-arrow-up text-white text-2xl absolute top-0 right-0 mr-2 mt-2"></i>
</>
@ -58,7 +75,7 @@ export function ProfileUpdateComponent() {
</div>
</div>
{/* Username Field */}
{/* Username Field
<div className="w-96">
<label className="block mb-2 text-gray-600">Username</label>
<input
@ -68,17 +85,17 @@ export function ProfileUpdateComponent() {
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
</div> */}
{/* Full Name Field */}
<div className="w-96">
<label className="block mb-2 text-gray-600">Full Name</label>
<label className="block mb-2 text-gray-600">username</label>
<input
type="text"
placeholder="Enter your full name"
placeholder="Enter your username"
className="input w-full"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
value={username}
onChange={(e) => setUserName(e.target.value)}
/>
</div>

View File

@ -1,36 +1,66 @@
import { ProfileUpdateComponent } from "./ProfileUpdateComponent";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect, useState } from "react";
export function ProfileUpdatePage() {
const [profile_pic, setProfilePic] = useState(undefined);
const [about, setAbout] = useState();
const [username, setUsernames] = useState();
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axiosInstance.get("/user/data/");
const fetchedProfilePic = response.data.profile_pic;
const fetchedAbout = response.data.about;
const fetchedUsernames = response.data.username;
setProfilePic(fetchedProfilePic);
setAbout(fetchedAbout);
setUsernames(fetchedUsernames);
} catch (error) {
console.error("Error fetching user:", error);
}
};
fetchUser();
}, []);
return (
<div>
<div className="stats shadow mt-3">
<div className="stat">
<div className="stat-title truncate">Username</div>
<div className="stat-value truncate">Sirin</div>
<div className="stat-desc truncate">User ID</div>
<div className="stat-value truncate">{username}</div>
{/* <div className="stat-desc truncate">User ID</div> */}
<div className="stat-figure text-secondary">
<div className="avatar online">
<div className="w-20 rounded-full">
<img src="https://us-tuna-sounds-images.voicemod.net/f322631f-689a-43ac-81ab-17a70f27c443-1692187175560.png" />
<img src={profile_pic} alt="Profile Picture" />
</div>
</div>
</div>
</div>
<div className="stat">
{/* <div className="stat">
<div className="stat-title">Health</div>
<div className="stat-value flex truncate">
234/3213
<div className="stat-figure text-secondary px-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="red" viewBox="0 0 24 24" className="inline-block w-8 h-8">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="red"
viewBox="0 0 24 24"
className="inline-block w-8 h-8"
>
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0"></path>
</svg>
</div>
</div>
<div className="stat-desc py-2">32% Remain</div>
<progress className="progress progress-error w-56" value={20} max="100"></progress>
</div>
<progress
className="progress progress-error w-56"
value={20}
max="100"
></progress>
</div> */}
{/*
<div className="stat">
<div className="stat-title truncate">Level</div>
<div className="stat-value flex">
@ -40,13 +70,18 @@ export function ProfileUpdatePage() {
xmlns="http://www.w3.org/2000/svg"
fill="#3abff8"
viewBox="0 0 24 24"
className="inline-block w-8 h-8">
className="inline-block w-8 h-8"
>
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
</div>
<div className="stat-desc py-2">3213/321312321 points</div>
<progress className="progress progress-info w-36" value="10" max="100"></progress>
<progress
className="progress progress-info w-36"
value="10"
max="100"
></progress>
</div>
<div className="stat">
@ -58,34 +93,40 @@ export function ProfileUpdatePage() {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block w-8 h-8 stroke-current">
className="inline-block w-8 h-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
stroke="gold"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
></path>
</svg>
</div>
</div>
<div className="stat-desc py-2">Top 12% of Global Ranking</div>
<progress className="progress progress-warning w-56" value={20} max="100"></progress>
</div>
<progress
className="progress progress-warning w-56"
value={20}
max="100"
></progress>
</div> */}
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">About me</h2>
<div className="card-actions justify-end"></div>
<textarea className="textarea textarea-bordered textarea-lg w-full" disabled>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum dolores recusandae, officiis consequuntur
nam, non ab commodi totam mollitia iusto nemo voluptatum error aliquam similique perspiciatis, eligendi
nulla. Animi, sit?
</textarea>
<textarea
className="textarea textarea-bordered textarea-lg w-full"
disabled
placeholder="Enter your about me"
value={about}
></textarea>
</div>
</div>
<div className="grid grid-cols-2 grid-rows-2 gap-4 my-2">
{/* <div className="grid grid-cols-2 grid-rows-2 gap-4 my-2">
<div className="col-span-full">
<div className="card bg-base-100 shadow">
<div className="card-body">
@ -110,18 +151,21 @@ export function ProfileUpdatePage() {
</div>
</div>
</div>
</div>
</div> */}
<div className="fixed bottom-4 right-4">
<ul className="menu menu-horizontal bg-base-200 rounded-box">
<li>
<a onClick={() => document.getElementById("my_modal_4").showModal()}>
<a
onClick={() => document.getElementById("my_modal_4").showModal()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="currentColor"
viewBox="0 0 16 16"
stroke="currentColor">
stroke="currentColor"
>
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
<p className="text-xl font-bold">Edit</p>
@ -135,7 +179,9 @@ export function ProfileUpdatePage() {
<div className="modal-box w-11/12 max-w-5xl flex flex-col">
<form method="dialog">
<ProfileUpdateComponent />
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
</div>
</dialog>