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 import viewsets
from rest_framework.permissions import IsAuthenticated
from ..models import Tag from ..models import Tag
from .serializers import TagSerializer from .serializers import TagSerializer
class TagViewSet(viewsets.ModelViewSet): class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all() 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) priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
def save(self, *args, **kwargs): 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: if self.completed and not self.completion_date:
self.completion_date = timezone.now() self.completion_date = timezone.now()
elif not self.completed: elif not self.completed:

View File

@ -1,9 +1,12 @@
from rest_framework import serializers from rest_framework import serializers
from users.models import CustomUser from users.models import CustomUser
from boards.models import ListBoard from boards.models import ListBoard
from tasks.models import Todo, RecurrenceTask, Habit from tasks.models import Todo, RecurrenceTask, Habit, Subtask
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
sub_task_count = serializers.SerializerMethodField()
class Meta: class Meta:
model = Todo model = Todo
fields = '__all__' fields = '__all__'
@ -19,6 +22,12 @@ class TaskSerializer(serializers.ModelSerializer):
validated_data['user'] = user validated_data['user'] = user
return Todo.objects.create(**validated_data) 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 TaskCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Todo model = Todo
@ -97,4 +106,14 @@ class HabitTaskSerializer(serializers.ModelSerializer):
class HabitTaskCreateSerializer(serializers.ModelSerializer): class HabitTaskCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Habit 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.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response 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 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, from tasks.tasks.serializers import (TaskCreateSerializer,
TaskSerializer, TaskSerializer,
RecurrenceTaskSerializer, RecurrenceTaskSerializer,
@ -32,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet):
return TaskCreateSerializer return TaskCreateSerializer
return TaskSerializer 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): def create(self, request, *args, **kwargs):
try: try:
new_task_data = request.data 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) 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): class RecurrenceTaskViewSet(viewsets.ModelViewSet):
queryset = RecurrenceTask.objects.all() queryset = RecurrenceTask.objects.all()
serializer_class = RecurrenceTaskSerializer 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 rest_framework.routers import DefaultRouter
from tasks.api import GoogleCalendarEventViewset 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 from tasks.misc.views import TagViewSet
@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet)
router.register(r'habit', HabitTaskViewSet) router.register(r'habit', HabitTaskViewSet)
router.register(r'tags', TagViewSet) router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events') router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
router.register(r'subtasks', SubTaskViewset, basename='subtasks')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),

View File

@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
Serializer for updating user profile. Serializer for updating user profile.
""" """
profile_pic = serializers.ImageField(required=False) 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) about = serializers.CharField(required=False)
class Meta: class Meta:
model = CustomUser 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): 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 rest_framework_simplejwt.tokens import RefreshToken
from users.serializers import CustomUserSerializer, UpdateProfileSerializer from users.serializers import CustomUserSerializer, UpdateProfileSerializer, UpdateProfileNopicSerializer
from users.models import CustomUser from users.models import CustomUser
class CustomUserCreate(APIView): class CustomUserCreate(APIView):
@ -57,13 +57,17 @@ class CustomUserProfileUpdate(APIView):
return Response ({ return Response ({
'error': 'User does not exist' 'error': 'User does not exist'
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
serializer = UpdateProfileSerializer(request.user, data=request.data) 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(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin): class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin):
queryset = CustomUser.objects.all() queryset = CustomUser.objects.all()
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@ -72,4 +76,4 @@ class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
serializer = self.get_serializer(request.user) serializer = self.get_serializer(request.user)
return Response(serializer.data) return Response(serializer.data)

View File

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

View File

@ -77,6 +77,9 @@ dependencies:
bootstrap: bootstrap:
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.3.2(@popperjs/core@2.11.8) version: 5.3.2(@popperjs/core@2.11.8)
date-fns:
specifier: ^2.30.0
version: 2.30.0
dotenv: dotenv:
specifier: ^16.3.1 specifier: ^16.3.1
version: 16.3.1 version: 16.3.1
@ -101,6 +104,9 @@ dependencies:
react-bootstrap: react-bootstrap:
specifier: ^2.9.1 specifier: ^2.9.1
version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) 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: react-datetime-picker:
specifier: ^5.5.3 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) 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: react-icons:
specifier: ^4.11.0 specifier: ^4.11.0
version: 4.12.0(react@18.2.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: react-router-dom:
specifier: ^6.18.0 specifier: ^6.18.0
version: 6.19.0(react-dom@18.2.0)(react@18.2.0) version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
@ -3473,6 +3482,22 @@ packages:
- '@types/react-dom' - '@types/react-dom'
dev: false 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): /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==} resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==}
peerDependencies: peerDependencies:
@ -3520,6 +3545,10 @@ packages:
scheduler: 0.23.0 scheduler: 0.23.0
dev: false 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): /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==} resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==}
peerDependencies: peerDependencies:
@ -3550,6 +3579,17 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -3565,6 +3605,41 @@ packages:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false 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): /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies: 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) => { export const deleteTask = (endpoint, id) => {
return axiosInstance return axiosInstance
.delete(`${baseURL}${endpoint}/${id}/`) .delete(`${baseURL}${endpoint}/${id}/`)
@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id);
// Update // Update
export const updateTodoTask = (id, data) => updateTask("todo", id, data); 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 updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
export const updateHabitTask = (id, data) => updateTask("habit", 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; return response.data;
} catch (error) { } catch (error) {
console.error("Error updating user profile:", error); console.error("Error updating user profile:", error);

View File

@ -1,5 +1,10 @@
import { useState, useEffect } from "react"; 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 { readTodoTasks } from "../../api/TaskApi";
import { axiosInstance } from "src/api/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) {
}; };
return ( 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 }}> <div className="flex" style={{ color: colorCode }}>
<span className="mx-2 mt-1">{icon}</span> <span className="mx-2 mt-1">{icon}</span>
<span>{name}</span> <span>{name}</span>
@ -39,10 +46,14 @@ function EachBlog({ name, colorCode, contentList, icon }) {
<input <input
type="checkbox" type="checkbox"
checked={item.completed} checked={item.completed}
className="checkbox mt-1 mr-2" className="checkbox mt-1 mr-2 bg-gray-300 "
onChange={() => handleCheckboxChange(index)} 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} {item.title}
</label> </label>
</div> </div>

View File

@ -5,6 +5,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import { getEvents, createEventId } from "./TaskDataHandler"; import { getEvents, createEventId } from "./TaskDataHandler";
import { axiosInstance } from "src/api/AxiosConfig";
export class Calendar extends React.Component { export class Calendar extends React.Component {
state = { state = {
@ -25,13 +26,13 @@ export class Calendar extends React.Component {
right: "dayGridMonth,timeGridWeek,timeGridDay", right: "dayGridMonth,timeGridWeek,timeGridDay",
}} }}
initialView="dayGridMonth" initialView="dayGridMonth"
editable={true} editable={false}
selectable={true} selectable={false}
selectMirror={true} selectMirror={true}
dayMaxEvents={true} dayMaxEvents={true}
weekends={this.state.weekendsVisible} weekends={this.state.weekendsVisible}
initialEvents={getEvents} initialEvents={getEvents}
select={this.handleDateSelect} // select={this.handleDateSelect}
eventContent={renderEventContent} eventContent={renderEventContent}
eventClick={this.handleEventClick} eventClick={this.handleEventClick}
eventsSet={this.handleEvents} eventsSet={this.handleEvents}
@ -85,22 +86,22 @@ export class Calendar extends React.Component {
}); });
}; };
handleDateSelect = (selectInfo) => { // handleDateSelect = (selectInfo) => {
let title = prompt("Please enter a new title for your event"); // let title = prompt("Please enter a new title for your event");
let calendarApi = selectInfo.view.calendar; // let calendarApi = selectInfo.view.calendar;
calendarApi.unselect(); // clear date selection // calendarApi.unselect(); // clear date selection
if (title) { // if (title) {
calendarApi.addEvent({ // calendarApi.addEvent({
id: createEventId(), // id: createEventId(),
title, // title,
start: selectInfo.startStr, // start: selectInfo.startStr,
end: selectInfo.endStr, // end: selectInfo.endStr,
allDay: selectInfo.allDay, // allDay: selectInfo.allDay,
}); // });
} // }
}; // };
handleEventClick = (clickInfo) => { handleEventClick = (clickInfo) => {
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { 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 [totalTask, setTotalTask] = useState(0);
const [totalCompletedTasks, settotalCompletedTasks] = useState(0); const [totalCompletedTasks, settotalCompletedTasks] = useState(0);
const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0); const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0);
const [totalTaskToday, setTotalTaskToday] = useState(0);
const [progressData, setProgressData] = useState(0); const [progressData, setProgressData] = useState(0);
const [overdueTask, setOverdueTask] = useState(0); const [overdueTask, setOverdueTask] = useState(0);
@ -36,19 +37,16 @@ export function Dashboard() {
const response = await axiosInstance.get("/dashboard/todostats/"); const response = await axiosInstance.get("/dashboard/todostats/");
const totalTaskValue = response.data.total_tasks || 0; const totalTaskValue = response.data.total_tasks || 0;
const totalCompletedTasksValue = response.data.total_completed_tasks || 0; const totalCompletedTasksValue = response.data.total_completed_tasks || 0;
const totalTaskTodayValue = response.data.total_task_today || 0;
const totalCompletedTasksTodayValue = const totalCompletedTasksTodayValue =
response.data.total_completed_tasks_today || 0; response.data.tasks_completed_today || 0;
const totalTaskToday = response.data.total_task_today || 0;
const totalCompletedTasksToday = response.data.tasks_completed_today || 0;
const overdueTasks = response.data.overdue_tasks || 0; const overdueTasks = response.data.overdue_tasks || 0;
const progress = (totalCompletedTasksToday / totalTaskToday) * 100;
const progress =
(totalCompletedTasksToday / totalCompletedTasksToday) * 100;
setTotalTask(totalTaskValue); setTotalTask(totalTaskValue);
settotalCompletedTasks(totalCompletedTasksValue); settotalCompletedTasks(totalCompletedTasksValue);
setTotalCompletedTasksToday(totalCompletedTasksTodayValue); setTotalCompletedTasksToday(totalCompletedTasksTodayValue);
setTotalTaskToday(totalTaskToday); setTotalTaskToday(totalTaskTodayValue);
setProgressData(progress); setProgressData(progress);
setOverdueTask(overdueTasks); setOverdueTask(overdueTasks);
}; };
@ -147,7 +145,11 @@ export function Dashboard() {
<Flex className="flex-col items-center"> <Flex className="flex-col items-center">
<ProgressCircle <ProgressCircle
className="mt-6" className="mt-6"
value={progressData} value={
isNaN(progressData) || !isFinite(progressData)
? 0
: `${progressData.toFixed(0)}%`
}
size={200} size={200}
strokeWidth={10} strokeWidth={10}
radius={60} radius={60}

View File

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

View File

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

View File

@ -1,16 +1,14 @@
import { useState } from "react"; import { useState } from "react";
import { useEffect } from "react";
import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { TaskDetailModal } from "./taskDetailModal"; import { TaskDetailModal } from "./taskDetailModal";
import { GoChecklist, GoArchive } from "react-icons/go";
export function TaskCard({ task, deleteTask, updateTask }) { export function TaskCard({ task, deleteTask, updateTask }) {
// State to track if the mouse is over the task card
const [mouseIsOver, setMouseIsOver] = useState(false); 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({ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
id: task.id, id: task.id,
data: { data: {
@ -18,68 +16,163 @@ export function TaskCard({ task, deleteTask, updateTask }) {
task, task,
}, },
}); });
// Style for the task card, adjusting for dragging animation
const style = { const style = {
transition, transition,
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
}; };
// ---- DESC AND TAG ---- */
if (task.tags === undefined) {
{ task.tags = [];
/* If card is dragged */
} }
// 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) { if (isDragging) {
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className=" className="opacity-30 bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative"
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 ( return (
<div> <div>
{/* Task Detail Modal */}
<TaskDetailModal <TaskDetailModal
taskId={task.id} taskId={task.id}
title={task.content} title={task.content}
description={task.description} description={task.description}
tags={task.tags} tags={task.tags}
difficulty={task.difficulty} difficulty={task.difficulty}
f challenge={task.challenge} challenge={task.challenge}
importance={task.importance} importance={task.importance}
updateTask={updateTask}
completed={task.completed}
/> />
{/* -------- Task Card -------- */}
<div <div
ref={setNodeRef} ref={setNodeRef}
{...attributes} {...attributes}
{...listeners} {...listeners}
style={style} 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={() => { onMouseEnter={() => {
setMouseIsOver(true); setMouseIsOver(true);
}} }}
onMouseLeave={() => { onMouseLeave={() => {
setMouseIsOver(false); setMouseIsOver(false);
}}> }}
<p onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white`} {/* -------- Task Content -------- */}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {/* Tags */}
{task.content} {tags}
</p> <div>
{/* Title */}
{mouseIsOver && ( <p
<button className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl bg-white font-semibold`}
onClick={() => { onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
deleteTask(task.id); {task.content}
}} </p>
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 "> {/* -------- Archive Task Button -------- */}
<BsFillTrashFill /> {mouseIsOver && (
</button> <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>
</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 { 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 { 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 [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance); const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); const [currentDifficulty, setCurrentDifficulty] = useState((difficulty - 1) * 25);
// console.log(currentDifficulty); const [selectedTags, setSelectedTags] = useState([]);
// console.log(isChallengeChecked); const [dateStart, setDateStart] = useState(new Date());
// console.log(isImportantChecked); 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); setChallengeChecked(!isChallengeChecked);
const data = {
challenge: !isChallengeChecked,
};
await updateTodoTaskPartial(taskId, data);
}; };
const handleImportantChange = () => { const handleImportantChange = async () => {
setImportantChecked(!isImportantChecked); setImportantChecked(!isImportantChecked);
const data = {
important: !isImportantChecked,
};
await updateTodoTaskPartial(taskId, data);
}; };
const handleDifficultyChange = (event) => { const handleDifficultyChange = async (event) => {
setCurrentDifficulty(parseInt(event.target.value, 10)); 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 ( return (
<dialog id={`task_detail_modal_${taskId}`} className="modal"> <dialog id={`task_detail_modal_${taskId}`} className="modal">
<div className="modal-box w-4/5 max-w-3xl"> <div className="modal-box w-4/5 max-w-3xl">
{/* Title */} {/* Title */}
<div className="flex flex-col py-2"> <div className="flex flex-col py-2">
<div className="flex flex-col"> <div className="flex flex-col">
<h3 className="font-bold text-lg"> {isTitleEditing ? (
<span className="flex gap-2"> <div className="flex gap-2 items-center">
{<FaTasks className="my-2" />} <FaTasks className="my-2" />
{title} <input
</span> type="text"
</h3> className="input-md input-bordered font-bold text-lg"
<p className="text-xs">{title}</p> 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>
</div> </div>
{/* Tags */} {/* Tags */}
<div className="flex flex-col py-2 pb-4"> <div className="flex flex-col py-2 pb-4">
<div className="flex flex-row space-x-5"> <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"> <label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
+ Add Tags + Add Tags
</label> </label>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> <ul tabIndex={0} className="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box w-52">
<li> {tags.map((tag, index) => (
<a> <li key={index}>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" /> <label className="cursor-pointer space-x-2">
Item 2 <input
</a> type="checkbox"
</li> checked={selectedTags.includes(tag)}
className="checkbox checkbox-sm"
onChange={() => handleTagChange(tag)}
/>
{tag}
</label>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>
<div className="flex flex-nowrap overflow-x-auto"></div> <div className="flex flex-nowrap overflow-x-auto">
{existingTags}
{selectedTagElements}
</div>
</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 */} {/* Description */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h2 className="font-bold"> <h2 className="font-bold">
@ -71,7 +380,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
{description} {description}
</textarea> </textarea>
</div> </div>
{/* Difficulty, Challenge, and Importance */} {/* Difficulty, Challenge, and Importance */}
<div className="flex flex-row space-x-3 my-4"> <div className="flex flex-row space-x-3 my-4">
<div className="flex-1 card shadow border-2 p-2"> <div className="flex-1 card shadow border-2 p-2">
@ -101,7 +409,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<input <input
type="checkbox" type="checkbox"
checked={isChallengeChecked} checked={isChallengeChecked}
className="checkbox" className="checkbox bg-black"
onChange={handleChallengeChange} onChange={handleChallengeChange}
/> />
</label> </label>
@ -116,14 +424,13 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<input <input
type="checkbox" type="checkbox"
checked={isImportantChecked} checked={isImportantChecked}
className="checkbox" className="checkbox bg-black"
onChange={handleImportantChange} onChange={handleImportantChange}
/> />
</label> </label>
</div> </div>
</div> </div>
</div> </div>
{/* Subtask */} {/* Subtask */}
<div className="flex flex-col pt-2"> <div className="flex flex-col pt-2">
<h2 className="font-bold"> <h2 className="font-bold">
@ -133,14 +440,21 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
</span> </span>
</h2> </h2>
<div className="flex space-x-3 pt-2"> <div className="flex space-x-3 pt-2">
<input type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" /> <input
<button className="btn"> 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 /> <FaPlus />
Add Subtask Add Subtask
</button> </button>
</div> </div>
{/* Display Subtasks */}
<div className="flex flex-col space-y-2 pt-2">{subtaskElements}</div>
</div> </div>
<form method="dialog"> <form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button> <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
</form> </form>

View File

@ -1,6 +1,8 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { apiUserLogout } from "src/api/AuthenticationApi"; import { apiUserLogout } from "src/api/AuthenticationApi";
import { useAuth } from "src/hooks/AuthHooks"; import { useAuth } from "src/hooks/AuthHooks";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect, useState } from "react";
const settings = { const settings = {
Profile: "/profile", Profile: "/profile",
@ -10,6 +12,7 @@ const settings = {
export function NavBar() { export function NavBar() {
const Navigate = useNavigate(); const Navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth(); const { isAuthenticated, setIsAuthenticated } = useAuth();
const [profile_pic, setProfilePic] = useState(undefined);
const logout = () => { const logout = () => {
apiUserLogout(); apiUserLogout();
@ -17,6 +20,25 @@ export function NavBar() {
Navigate("/"); 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 ( return (
<div data-theme="night" className="navbar bg-base-100"> <div data-theme="night" className="navbar bg-base-100">
<div className="flex-1"> <div className="flex-1">
@ -32,19 +54,23 @@ export function NavBar() {
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar"> <label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full"> <div className="w-10 rounded-full">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png" /> <img src={profile_pic} />
</div> </div>
</label> </label>
<ul <ul
tabIndex={0} 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> <li>
<a href={settings.Profile} className="justify-between"> <a
onClick={() => Navigate(settings.Profile)}
className="justify-between"
>
Profile Profile
</a> </a>
</li> </li>
<li> <li>
<a href={settings.Account}>Settings</a> <a onClick={() => Navigate(settings.Account)}>Settings</a>
</li> </li>
<li> <li>
<a onClick={logout}>Logout</a> <a onClick={logout}>Logout</a>
@ -53,10 +79,16 @@ export function NavBar() {
</div> </div>
) : ( ) : (
<div className="flex gap-2"> <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 Login
</button> </button>
<button className="btn btn-success" onClick={() => Navigate("/signup")}> <button
className="btn btn-success"
onClick={() => Navigate("/signup")}
>
Sign Up Sign Up
</button> </button>
</div> </div>

View File

@ -1,13 +1,30 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { ApiUpdateUserProfile } from "src/api/UserProfileApi"; import { ApiUpdateUserProfile } from "src/api/UserProfileApi";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect } from "react";
export function ProfileUpdateComponent() { export function ProfileUpdateComponent() {
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [username, setUsername] = useState(""); const [username, setUserName] = useState("");
const [fullName, setFullName] = useState(""); const [about, setAbout] = useState();
const [about, setAbout] = useState("");
const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg";
const fileInputRef = useRef(null); 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 = () => { const handleImageUpload = () => {
if (fileInputRef.current) { if (fileInputRef.current) {
@ -25,7 +42,7 @@ export function ProfileUpdateComponent() {
const handleSave = () => { const handleSave = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("profile_pic", file); formData.append("profile_pic", file);
formData.append("first_name", username); formData.append("username", username);
formData.append("about", about); formData.append("about", about);
ApiUpdateUserProfile(formData); ApiUpdateUserProfile(formData);
@ -50,7 +67,7 @@ export function ProfileUpdateComponent() {
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" /> <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-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> <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>
</div> </div>
{/* Username Field */} {/* Username Field
<div className="w-96"> <div className="w-96">
<label className="block mb-2 text-gray-600">Username</label> <label className="block mb-2 text-gray-600">Username</label>
<input <input
@ -68,17 +85,17 @@ export function ProfileUpdateComponent() {
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
</div> </div> */}
{/* Full Name Field */} {/* Full Name Field */}
<div className="w-96"> <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 <input
type="text" type="text"
placeholder="Enter your full name" placeholder="Enter your username"
className="input w-full" className="input w-full"
value={fullName} value={username}
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setUserName(e.target.value)}
/> />
</div> </div>

View File

@ -1,36 +1,66 @@
import { ProfileUpdateComponent } from "./ProfileUpdateComponent"; import { ProfileUpdateComponent } from "./ProfileUpdateComponent";
import { axiosInstance } from "src/api/AxiosConfig";
import { useEffect, useState } from "react";
export function ProfileUpdatePage() { 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 ( return (
<div> <div>
<div className="stats shadow mt-3"> <div className="stats shadow mt-3">
<div className="stat"> <div className="stat">
<div className="stat-title truncate">Username</div> <div className="stat-title truncate">Username</div>
<div className="stat-value truncate">Sirin</div> <div className="stat-value truncate">{username}</div>
<div className="stat-desc truncate">User ID</div> {/* <div className="stat-desc truncate">User ID</div> */}
<div className="stat-figure text-secondary"> <div className="stat-figure text-secondary">
<div className="avatar online"> <div className="avatar online">
<div className="w-20 rounded-full"> <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> </div>
</div> </div>
<div className="stat"> {/* <div className="stat">
<div className="stat-title">Health</div> <div className="stat-title">Health</div>
<div className="stat-value flex truncate"> <div className="stat-value flex truncate">
234/3213 234/3213
<div className="stat-figure text-secondary px-2"> <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> <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> </svg>
</div> </div>
</div> </div>
<div className="stat-desc py-2">32% Remain</div> <div className="stat-desc py-2">32% Remain</div>
<progress className="progress progress-error w-56" value={20} max="100"></progress> <progress
</div> className="progress progress-error w-56"
value={20}
max="100"
></progress>
</div> */}
{/*
<div className="stat"> <div className="stat">
<div className="stat-title truncate">Level</div> <div className="stat-title truncate">Level</div>
<div className="stat-value flex"> <div className="stat-value flex">
@ -40,13 +70,18 @@ export function ProfileUpdatePage() {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="#3abff8" fill="#3abff8"
viewBox="0 0 24 24" 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> <path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg> </svg>
</div> </div>
</div> </div>
<div className="stat-desc py-2">3213/321312321 points</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>
<div className="stat"> <div className="stat">
@ -58,34 +93,40 @@ export function ProfileUpdatePage() {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="inline-block w-8 h-8 stroke-current"> className="inline-block w-8 h-8 stroke-current"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
stroke="gold" 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> </svg>
</div> </div>
</div> </div>
<div className="stat-desc py-2">Top 12% of Global Ranking</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> <progress
</div> className="progress progress-warning w-56"
value={20}
max="100"
></progress>
</div> */}
</div> </div>
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div className="card-body"> <div className="card-body">
<h2 className="card-title">About me</h2> <h2 className="card-title">About me</h2>
<div className="card-actions justify-end"></div> <div className="card-actions justify-end"></div>
<textarea className="textarea textarea-bordered textarea-lg w-full" disabled> <textarea
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum dolores recusandae, officiis consequuntur className="textarea textarea-bordered textarea-lg w-full"
nam, non ab commodi totam mollitia iusto nemo voluptatum error aliquam similique perspiciatis, eligendi disabled
nulla. Animi, sit? placeholder="Enter your about me"
</textarea> value={about}
></textarea>
</div> </div>
</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="col-span-full">
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div className="card-body"> <div className="card-body">
@ -110,18 +151,21 @@ export function ProfileUpdatePage() {
</div> </div>
</div> </div>
</div> </div>
</div> </div> */}
<div className="fixed bottom-4 right-4"> <div className="fixed bottom-4 right-4">
<ul className="menu menu-horizontal bg-base-200 rounded-box"> <ul className="menu menu-horizontal bg-base-200 rounded-box">
<li> <li>
<a onClick={() => document.getElementById("my_modal_4").showModal()}> <a
onClick={() => document.getElementById("my_modal_4").showModal()}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5" className="h-5 w-5"
fill="currentColor" fill="currentColor"
viewBox="0 0 16 16" 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" /> <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> </svg>
<p className="text-xl font-bold">Edit</p> <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"> <div className="modal-box w-11/12 max-w-5xl flex flex-col">
<form method="dialog"> <form method="dialog">
<ProfileUpdateComponent /> <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> </form>
</div> </div>
</dialog> </dialog>