mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Merge branch 'main' into feature/kanban-board
This commit is contained in:
commit
e19f60f542
@ -6,46 +6,22 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from tasks.utils import get_service
|
from tasks.utils import get_service, generate_recurrence_rule
|
||||||
from tasks.models import Todo, RecurrenceTask
|
from tasks.models import Todo, RecurrenceTask
|
||||||
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
|
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
||||||
|
"""Viewset for list or save Google Calendar Events."""
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_time = datetime.now(tz=timezone.utc).isoformat()
|
self.current_time = (datetime.now(tz=timezone.utc) + timedelta(days=-7)).isoformat()
|
||||||
self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)'
|
self.max_time = (datetime.now(tz=timezone.utc) + timedelta(days=7)).isoformat()
|
||||||
|
self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end,originalStartTime)'
|
||||||
def _validate_serializer(self, serializer):
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response("Validate Successfully", status=200)
|
|
||||||
return Response(serializer.errors, status=400)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
service = get_service(request)
|
|
||||||
events = service.events().list(calendarId='primary', fields=self.event_fields).execute()
|
|
||||||
for event in events.get('items', []):
|
|
||||||
if event.get('recurringEventId'):
|
|
||||||
continue
|
|
||||||
event['start_datetime'] = event.get('start').get('dateTime')
|
|
||||||
event['end_datetime'] = event.get('end').get('dateTime')
|
|
||||||
event.pop('start')
|
|
||||||
event.pop('end')
|
|
||||||
try:
|
|
||||||
task = Todo.objects.get(google_calendar_id=event['id'])
|
|
||||||
serializer = TodoUpdateSerializer(instance=task, data=event)
|
|
||||||
return self._validate_serializer(serializer)
|
|
||||||
except Todo.DoesNotExist:
|
|
||||||
serializer = TodoUpdateSerializer(data=event, user=request.user)
|
|
||||||
return self._validate_serializer(serializer)
|
|
||||||
|
|
||||||
def list(self, request, days=7):
|
|
||||||
max_time = (datetime.now(tz=timezone.utc) + timedelta(days=days)).isoformat()
|
|
||||||
|
|
||||||
|
def _get_google_events(self, request):
|
||||||
|
"""Get all events from Google Calendar. """
|
||||||
service = get_service(request)
|
service = get_service(request)
|
||||||
events = []
|
events = []
|
||||||
next_page_token = None
|
next_page_token = None
|
||||||
@ -54,12 +30,12 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
query = service.events().list(
|
query = service.events().list(
|
||||||
calendarId='primary',
|
calendarId='primary',
|
||||||
timeMin=self.current_time,
|
timeMin=self.current_time,
|
||||||
timeMax=max_time,
|
timeMax=self.max_time,
|
||||||
maxResults=200,
|
maxResults=200,
|
||||||
singleEvents=True,
|
singleEvents=True,
|
||||||
orderBy='startTime',
|
orderBy='startTime',
|
||||||
pageToken=next_page_token,
|
pageToken=next_page_token,
|
||||||
fields='items(id,summary,description,created,recurringEventId,updated,start,end)',
|
fields=self.event_fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
page_results = query.execute()
|
page_results = query.execute()
|
||||||
@ -71,4 +47,60 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
if next_page_token is None:
|
if next_page_token is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
return Response(events, status=200)
|
return events
|
||||||
|
|
||||||
|
def _validate_serializer(self, serializer):
|
||||||
|
"""Validate serializer and return response."""
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response("Validate Successfully", status=200)
|
||||||
|
return Response(serializer.errors, status=400)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""Create a new Google Calendar Event."""
|
||||||
|
events = self._get_google_events(request)
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
recurrence_task_ids = []
|
||||||
|
for event in events:
|
||||||
|
start_datetime = event.get('start', {}).get('dateTime')
|
||||||
|
end_datetime = event.get('end', {}).get('dateTime')
|
||||||
|
|
||||||
|
event['start_datetime'] = start_datetime
|
||||||
|
event['end_datetime'] = end_datetime
|
||||||
|
event.pop('start')
|
||||||
|
event.pop('end')
|
||||||
|
|
||||||
|
if (event.get('recurringEventId') in recurrence_task_ids):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (event.get('recurringEventId') is not None):
|
||||||
|
originalStartTime = event.get('originalStartTime', {}).get('dateTime')
|
||||||
|
rrule_text = generate_recurrence_rule(event['start_datetime'], event['end_datetime'], originalStartTime)
|
||||||
|
event['recurrence'] = rrule_text
|
||||||
|
event.pop('originalStartTime')
|
||||||
|
recurrence_task_ids.append(event['recurringEventId'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
task = RecurrenceTask.objects.get(google_calendar_id=event['id'])
|
||||||
|
serializer = RecurrenceTaskUpdateSerializer(instance=task, data=event)
|
||||||
|
except RecurrenceTask.DoesNotExist:
|
||||||
|
serializer = RecurrenceTaskUpdateSerializer(data=event, user=request.user)
|
||||||
|
|
||||||
|
responses.append(self._validate_serializer(serializer))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
task = Todo.objects.get(google_calendar_id=event['id'])
|
||||||
|
serializer = TodoUpdateSerializer(instance=task, data=event)
|
||||||
|
except Todo.DoesNotExist:
|
||||||
|
serializer = TodoUpdateSerializer(data=event, user=request.user)
|
||||||
|
|
||||||
|
responses.append(self._validate_serializer(serializer))
|
||||||
|
|
||||||
|
return responses[0] if responses else Response("No events to process", status=200)
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
"""List all Google Calendar Events."""
|
||||||
|
return Response(self._get_google_events(request), status=200)
|
||||||
|
|
||||||
39
backend/tasks/migrations/0012_habit.py
Normal file
39
backend/tasks/migrations/0012_habit.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-13 18:15
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('tasks', '0011_recurrencetask'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Habit',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.TextField()),
|
||||||
|
('notes', models.TextField(default='')),
|
||||||
|
('importance', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=1)),
|
||||||
|
('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1)),
|
||||||
|
('challenge', models.BooleanField(default=False)),
|
||||||
|
('fromSystem', models.BooleanField(default=False)),
|
||||||
|
('creation_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_update', models.DateTimeField(auto_now=True)),
|
||||||
|
('google_calendar_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('start_event', models.DateTimeField(null=True)),
|
||||||
|
('end_event', models.DateTimeField(null=True)),
|
||||||
|
('streak', models.IntegerField(default=0)),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='tasks.tag')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-14 15:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tasks', '0012_habit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='recurrence_rule',
|
||||||
|
field=models.CharField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -68,11 +68,19 @@ class Todo(Task):
|
|||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class RecurrenceTask(Task):
|
class RecurrenceTask(Task):
|
||||||
recurrence_rule = models.TextField()
|
recurrence_rule = models.CharField()
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.title} ({self.recurrence_rule})"
|
return f"{self.title} ({self.recurrence_rule})"
|
||||||
|
|
||||||
|
|
||||||
|
class Habit(Task):
|
||||||
|
streak = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.title} ({self.streak})"
|
||||||
|
|
||||||
|
|
||||||
class Subtask(models.Model):
|
class Subtask(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a subtask associated with a task.
|
Represents a subtask associated with a task.
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class RecurrenceTaskUpdateSerializer(serializers.ModelSerializer):
|
|||||||
description = serializers.CharField(source="notes", required=False)
|
description = serializers.CharField(source="notes", required=False)
|
||||||
created = serializers.DateTimeField(source="creation_date")
|
created = serializers.DateTimeField(source="creation_date")
|
||||||
updated = serializers.DateTimeField(source="last_update")
|
updated = serializers.DateTimeField(source="last_update")
|
||||||
recurrence = serializers.DateTimeField(source="recurrence_rule")
|
recurrence = serializers.CharField(source="recurrence_rule")
|
||||||
start_datetime = serializers.DateTimeField(source="start_event", required=False)
|
start_datetime = serializers.DateTimeField(source="start_event", required=False)
|
||||||
end_datetime = serializers.DateTimeField(source="end_event", required=False)
|
end_datetime = serializers.DateTimeField(source="end_event", required=False)
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from ..models import Todo
|
from ..models import Todo, RecurrenceTask, Habit
|
||||||
|
|
||||||
class TaskCreateSerializer(serializers.ModelSerializer):
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
|
||||||
model = Todo
|
|
||||||
# fields = '__all__'
|
|
||||||
exclude = ('tags',)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
# Create a new task with validated data
|
|
||||||
return Todo.objects.create(**validated_data)
|
|
||||||
|
|
||||||
class TaskGeneralSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Todo
|
model = Todo
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -19,3 +9,39 @@ class TaskGeneralSerializer(serializers.ModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# Create a new task with validated data
|
# Create a new task with validated data
|
||||||
return Todo.objects.create(**validated_data)
|
return Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
|
class TaskCreateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Todo
|
||||||
|
exclude = ('tags',)
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrenceTaskSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RecurrenceTask
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Create a new task with validated data
|
||||||
|
return Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
|
class RecurrenceTaskCreateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RecurrenceTask
|
||||||
|
exclude = ('tags',)
|
||||||
|
|
||||||
|
|
||||||
|
class HabitTaskSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Habit
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Create a new task with validated data
|
||||||
|
return Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class HabitTaskCreateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Habit
|
||||||
|
exclude = ('tags',)
|
||||||
@ -1,16 +1,49 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from tasks.models import Todo
|
from tasks.models import Todo, RecurrenceTask, Habit
|
||||||
from .serializers import TaskCreateSerializer, TaskGeneralSerializer
|
from tasks.tasks.serializers import (TaskCreateSerializer,
|
||||||
|
TaskSerializer,
|
||||||
|
RecurrenceTaskSerializer,
|
||||||
|
RecurrenceTaskCreateSerializer,
|
||||||
|
HabitTaskSerializer,
|
||||||
|
HabitTaskCreateSerializer)
|
||||||
|
|
||||||
|
|
||||||
class TodoViewSet(viewsets.ModelViewSet):
|
class TodoViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Todo.objects.all()
|
queryset = Todo.objects.all()
|
||||||
serializer_class = TaskGeneralSerializer
|
serializer_class = TaskSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = Todo.objects.filter(user=self.request.user)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
# Can't add ManytoMany at creation time (Tags)
|
# Can't add ManytoMany at creation time (Tags)
|
||||||
if self.action == 'create':
|
if self.action == 'create':
|
||||||
return TaskCreateSerializer
|
return TaskCreateSerializer
|
||||||
return TaskGeneralSerializer
|
return TaskSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrenceTaskViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = RecurrenceTask.objects.all()
|
||||||
|
serializer_class = RecurrenceTaskSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
# Can't add ManytoMany at creation time (Tags)
|
||||||
|
if self.action == 'create':
|
||||||
|
return RecurrenceTaskCreateSerializer
|
||||||
|
return RecurrenceTaskSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class HabitTaskViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Habit.objects.all()
|
||||||
|
serializer_class = HabitTaskSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
# Can't add ManytoMany at creation time (Tags)
|
||||||
|
if self.action == 'create':
|
||||||
|
return HabitTaskCreateSerializer
|
||||||
|
return HabitTaskSerializer
|
||||||
@ -3,12 +3,14 @@ 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
|
from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet
|
||||||
from tasks.misc.views import TagViewSet
|
from tasks.misc.views import TagViewSet
|
||||||
|
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'todo', TodoViewSet)
|
router.register(r'todo', TodoViewSet)
|
||||||
|
router.register(r'daily', RecurrenceTaskViewSet)
|
||||||
|
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')
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,55 @@
|
|||||||
|
from dateutil import rrule
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
from authentications.access_token_cache import get_credential_from_cache_token
|
from authentications.access_token_cache import get_credential_from_cache_token
|
||||||
|
|
||||||
|
|
||||||
def get_service(request):
|
def get_service(request):
|
||||||
|
"""
|
||||||
|
Get a service that communicates to a Google API.
|
||||||
|
|
||||||
|
:param request: Http request object
|
||||||
|
:return: A Resource object with methods for interacting with the calendar service
|
||||||
|
"""
|
||||||
credentials = get_credential_from_cache_token(request.user.id)
|
credentials = get_credential_from_cache_token(request.user.id)
|
||||||
return build('calendar', 'v3', credentials=credentials)
|
return build('calendar', 'v3', credentials=credentials)
|
||||||
|
|
||||||
|
def _determine_frequency(time_difference):
|
||||||
|
if time_difference.days >= 365:
|
||||||
|
return rrule.YEARLY
|
||||||
|
elif time_difference.days >= 30:
|
||||||
|
return rrule.MONTHLY
|
||||||
|
elif time_difference.days >= 7:
|
||||||
|
return rrule.WEEKLY
|
||||||
|
elif time_difference.days >= 1:
|
||||||
|
return rrule.DAILY
|
||||||
|
elif time_difference.seconds >= 3600:
|
||||||
|
return rrule.HOURLY
|
||||||
|
elif time_difference.seconds >= 60:
|
||||||
|
return rrule.MINUTELY
|
||||||
|
else:
|
||||||
|
return rrule.SECONDLY
|
||||||
|
|
||||||
|
def generate_recurrence_rule(datetime1: str, datetime2: str, original_start_time: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate recurrence rule from
|
||||||
|
difference between two datetime string.
|
||||||
|
|
||||||
|
:param task1: A task object
|
||||||
|
:param task2: A task object
|
||||||
|
:return: A recurrence rule string according to ICAL format
|
||||||
|
"""
|
||||||
|
start_time1 = datetime.fromisoformat(datetime1)
|
||||||
|
start_time2 = datetime.fromisoformat(datetime2)
|
||||||
|
|
||||||
|
time_difference = start_time2 - start_time1
|
||||||
|
|
||||||
|
recurrence_rule = rrule.rrule(
|
||||||
|
freq=_determine_frequency(time_difference),
|
||||||
|
dtstart=datetime.fromisoformat(original_start_time),
|
||||||
|
interval=time_difference.days if time_difference.days > 0 else 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return str(recurrence_rule)
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-13 18:15
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import users.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0004_userstats'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='endurance',
|
||||||
|
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='intelligence',
|
||||||
|
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='luck',
|
||||||
|
field=models.IntegerField(default=users.models.random_luck, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='perception',
|
||||||
|
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='strength',
|
||||||
|
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -4,13 +4,15 @@ import { Route, Routes, useLocation } from "react-router-dom";
|
|||||||
import TestAuth from "./components/testAuth";
|
import TestAuth from "./components/testAuth";
|
||||||
import LoginPage from "./components/authentication/LoginPage";
|
import LoginPage from "./components/authentication/LoginPage";
|
||||||
import SignUpPage from "./components/authentication/SignUpPage";
|
import SignUpPage from "./components/authentication/SignUpPage";
|
||||||
import NavBar from "./components/navigators/Navbar";
|
import NavBar from "./components/navigations/Navbar";
|
||||||
import Home from "./components/Home";
|
import Home from "./components/Home";
|
||||||
import ProfileUpdate from "./components/ProfileUpdatePage";
|
|
||||||
import Calendar from "./components/calendar/calendar";
|
import Calendar from "./components/calendar/calendar";
|
||||||
import KanbanBoard from "./components/kanbanBoard/kanbanBoard";
|
import KanbanPage from "./components/kanbanBoard/kanbanPage";
|
||||||
import IconSideNav from "./components/IconSideNav";
|
import IconSideNav from "./components/navigations/IconSideNav";
|
||||||
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
|
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
|
||||||
|
import PrivateRoute from "./PrivateRoute";
|
||||||
|
import ProfileUpdatePage from "./components/profilePage";
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -20,16 +22,24 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={isLoginPageOrSignUpPage ? "" : "display: flex"}>
|
<div className={isLoginPageOrSignUpPage ? "" : "display: flex"}>
|
||||||
{!isLoginPageOrSignUpPage && <IconSideNav />}
|
{!isLoginPageOrSignUpPage && <IconSideNav />}
|
||||||
<div className={isLoginPageOrSignUpPage ? "" : "flex-1"}>
|
<div className={isLoginPageOrSignUpPage ? "" : "flex-1 ml-[76px] overflow-hidden"}>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className={isLoginPageOrSignUpPage ? "" : "flex items-center justify-center"}>
|
<div className={isLoginPageOrSignUpPage ? "" : "overflow-x-auto"}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/tasks" element={<KanbanBoard />} />
|
<Route exact path="/tasks" element={<PrivateRoute />}>
|
||||||
|
<Route exact path="/tasks" element={<KanbanPage />} />
|
||||||
|
</Route>
|
||||||
<Route path="/testAuth" element={<TestAuth />} />
|
<Route path="/testAuth" element={<TestAuth />} />
|
||||||
<Route path="/update_profile" element={<ProfileUpdate />} />
|
<Route exact path="/profile" element={<PrivateRoute />}>
|
||||||
<Route path="/calendar" element={<Calendar />} />
|
<Route exact path="/profile" element={<ProfileUpdatePage />} />
|
||||||
<Route path="/priority" element={<Eisenhower />} />
|
</Route>
|
||||||
|
<Route exact path="/calendar" element={<PrivateRoute />}>
|
||||||
|
<Route exact path="/calendar" element={<Calendar />} />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/priority" element={<PrivateRoute />}>
|
||||||
|
<Route exact path="/priority" element={<Eisenhower />} />
|
||||||
|
</Route>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/signup" element={<SignUpPage />} />
|
<Route path="/signup" element={<SignUpPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
11
frontend/src/PrivateRoute.jsx
Normal file
11
frontend/src/PrivateRoute.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
|
import { useAuth } from "./hooks/authentication/IsAuthenticated";
|
||||||
|
|
||||||
|
const PrivateRoute = () => {
|
||||||
|
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
||||||
|
const auth = isAuthenticated;
|
||||||
|
return auth ? <Outlet /> : <Navigate to="/login" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
@ -1,12 +1,8 @@
|
|||||||
import axiosInstance from "./configs/AxiosConfig";
|
import { createTask, readTasks, readTaskByID, updateTask, deleteTask } from "./TaskApi";
|
||||||
|
|
||||||
export const fetchTags = () => {
|
// CRUD functions for "tags" endpoint
|
||||||
return axiosInstance
|
export const createTag = data => createTask("tags", data);
|
||||||
.get("tags/")
|
export const readTags = () => readTasks("tags");
|
||||||
.then(response => {
|
export const readTagByID = id => readTaskByID("tags", id);
|
||||||
return response.data;
|
export const updateTag = (id, data) => updateTask("tags", id, data);
|
||||||
})
|
export const deleteTag = id => deleteTask("tags", id);
|
||||||
.catch(error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,23 +1,73 @@
|
|||||||
import axiosInstance from "./configs/AxiosConfig";
|
import axiosInstance from "./configs/AxiosConfig";
|
||||||
|
|
||||||
export const fetchTodoTasks = () => {
|
const baseURL = "";
|
||||||
|
|
||||||
|
export const createTask = (endpoint, data) => {
|
||||||
return axiosInstance
|
return axiosInstance
|
||||||
.get("todo/")
|
.post(`${baseURL}${endpoint}/`, data)
|
||||||
.then(response => {
|
.then(response => response.data)
|
||||||
return response.data;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTodoTasksID = id => {
|
export const readTasks = endpoint => {
|
||||||
return axiosInstance
|
return axiosInstance
|
||||||
.get(`todo/${id}/`)
|
.get(`${baseURL}${endpoint}/`)
|
||||||
.then(response => {
|
.then(response => response.data)
|
||||||
return response.data;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const readTaskByID = (endpoint, id) => {
|
||||||
|
return axiosInstance
|
||||||
|
.get(`${baseURL}${endpoint}/${id}/`)
|
||||||
|
.then(response => response.data)
|
||||||
|
.catch(error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTask = (endpoint, id, data) => {
|
||||||
|
return axiosInstance
|
||||||
|
.put(`${baseURL}${endpoint}/${id}/`, data)
|
||||||
|
.then(response => response.data)
|
||||||
|
.catch(error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTask = (endpoint, id) => {
|
||||||
|
return axiosInstance
|
||||||
|
.delete(`${baseURL}${endpoint}/${id}/`)
|
||||||
|
.then(response => response.data)
|
||||||
|
.catch(error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create
|
||||||
|
export const createTodoTask = data => createTask("todo", data);
|
||||||
|
export const createRecurrenceTask = data => createTask("daily", data);
|
||||||
|
export const createHabitTask = data => createTask("habit", data);
|
||||||
|
|
||||||
|
// Read
|
||||||
|
export const readTodoTasks = () => readTasks("todo");
|
||||||
|
export const readRecurrenceTasks = () => readTasks("daily");
|
||||||
|
export const readHabitTasks = () => readTasks("habit");
|
||||||
|
|
||||||
|
// Read by ID
|
||||||
|
export const readTodoTaskByID = id => readTaskByID("todo", id);
|
||||||
|
export const readRecurrenceTaskByID = id => readTaskByID("daily", id);
|
||||||
|
export const readHabitTaskByID = id => readTaskByID("habit", id);
|
||||||
|
|
||||||
|
// Update
|
||||||
|
export const updateTodoTask = (id, data) => updateTask("todo", id, data);
|
||||||
|
export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
|
||||||
|
export const updateHabitTask = (id, data) => updateTask("habit", id, data);
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
export const deleteTodoTask = id => deleteTask("todo", id);
|
||||||
|
export const deleteRecurrenceTask = id => deleteTask("daily", id);
|
||||||
|
export const deleteHabitTask = id => deleteTask("habit", id);
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
|
|
||||||
const ApiUpdateUserProfile = async (formData) => {
|
const ApiUpdateUserProfile = async formData => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://127.0.1:8000/api/user/update/', formData, {
|
const response = await axios.post("http://127.0.1:8000/api/user/update/", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': "Bearer " + localStorage.getItem('access_token'),
|
Authorization: "Bearer " + localStorage.getItem("access_token"),
|
||||||
'Content-Type': 'multipart/form-data',
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ const ApiUpdateUserProfile = async (formData) => {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user profile:', error);
|
console.error("Error updating user profile:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
|
import { redirect } from "react-router-dom";
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: 'http://127.0.0.1:8000/api/',
|
baseURL: "http://127.0.0.1:8000/api/",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': "Bearer " + localStorage.getItem('access_token'),
|
Authorization: "Bearer " + localStorage.getItem("access_token"),
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
'accept': 'application/json',
|
accept: "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -15,28 +16,30 @@ axiosInstance.interceptors.response.use(
|
|||||||
response => response,
|
response => response,
|
||||||
error => {
|
error => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
const refresh_token = localStorage.getItem('refresh_token');
|
const refresh_token = localStorage.getItem("refresh_token");
|
||||||
|
|
||||||
// Check if the error is due to 401 and a refresh token is available
|
// Check if the error is due to 401 and a refresh token is available
|
||||||
if (error.response.status === 401 && error.response.statusText === "Unauthorized" && refresh_token !== "undefined") {
|
if (
|
||||||
|
error.response.status === 401 &&
|
||||||
|
error.response.statusText === "Unauthorized" &&
|
||||||
|
refresh_token !== "undefined"
|
||||||
|
) {
|
||||||
return axiosInstance
|
return axiosInstance
|
||||||
.post('/token/refresh/', { refresh: refresh_token })
|
.post("/token/refresh/", { refresh: refresh_token })
|
||||||
.then((response) => {
|
.then(response => {
|
||||||
|
localStorage.setItem("access_token", response.data.access);
|
||||||
|
|
||||||
localStorage.setItem('access_token', response.data.access);
|
axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access;
|
||||||
|
originalRequest.headers["Authorization"] = "Bearer " + response.data.access;
|
||||||
axiosInstance.defaults.headers['Authorization'] = "Bearer " + response.data.access;
|
|
||||||
originalRequest.headers['Authorization'] = "Bearer " + response.data.access;
|
|
||||||
|
|
||||||
return axiosInstance(originalRequest);
|
return axiosInstance(originalRequest);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log('Interceptors error: ', err);
|
console.log("Interceptors error: ", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export default axiosInstance;
|
export default axiosInstance;
|
||||||
|
|||||||
@ -1,27 +1,77 @@
|
|||||||
import React from 'react';
|
import React, { useState } from "react";
|
||||||
|
import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi";
|
||||||
|
|
||||||
|
function EachBlog({ name, colorCode, contentList, icon }) {
|
||||||
|
const [tasks, setTasks] = useState(contentList);
|
||||||
|
|
||||||
|
const handleCheckboxChange = index => {
|
||||||
|
const updatedTasks = [...tasks];
|
||||||
|
updatedTasks[index].checked = !updatedTasks[index].checked;
|
||||||
|
setTasks(updatedTasks);
|
||||||
|
};
|
||||||
|
|
||||||
function EachBlog({ name, colorCode }) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`grid grid-rows-2 gap-4 text-left p-4 rounded-lg bg-white border border-gray-300 shadow-md`}>
|
<div className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}>
|
||||||
<div className={`text-xl font-bold`} style={{ color: colorCode }}>
|
<div className="flex" style={{ color: colorCode }}>
|
||||||
{name}
|
<span className="mx-2 mt-1">{icon}</span>
|
||||||
|
<span>{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-36'>
|
<hr className="my-3 h-0.5 border-t-0 bg-gray-300 opacity-100 dark:opacity-50" />
|
||||||
Content goes here
|
<div className="space-y-2">
|
||||||
|
{tasks.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center">No tasks</p>
|
||||||
|
) : (
|
||||||
|
tasks.map((item, index) => (
|
||||||
|
<div key={index} className="flex items-start">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.checked}
|
||||||
|
className="checkbox mt-1 mr-2"
|
||||||
|
onChange={() => handleCheckboxChange(index)}
|
||||||
|
/>
|
||||||
|
<label className="cursor-pointer">{item.text}</label>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Eisenhower() {
|
function Eisenhower() {
|
||||||
|
const contentList_ui = [
|
||||||
|
{ text: "Complete report for the meeting", checked: true },
|
||||||
|
{ text: "Review project proposal", checked: false },
|
||||||
|
{ text: "Submit expense report", checked: false },
|
||||||
|
{ text: "Complete report for the meeting", checked: true },
|
||||||
|
{ text: "Review project proposal", checked: false },
|
||||||
|
{ text: "Submit expense report", checked: false },
|
||||||
|
{ text: "Complete report for the meeting", checked: true },
|
||||||
|
{ text: "Review project proposal", checked: false },
|
||||||
|
{ text: "Submit expense report", checked: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const contentList_uni = [];
|
||||||
|
const contentList_nui = [];
|
||||||
|
const contentList_nuni = [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-slate-100 text-left p-4 m-auto'>
|
<div className="bg-slate-100 text-left p-4 w-full">
|
||||||
<h1 className="text-3xl font-bold mb-4">The Eisenhower Matrix</h1>
|
<div className="grid grid-rows-2 grid-cols-2 gap-2">
|
||||||
<div className='grid grid-rows-2 grid-cols-2 gap-2'>
|
<EachBlog name="Urgent & Important" colorCode="#ff5f68" icon={<FiAlertCircle />} contentList={contentList_ui} />
|
||||||
<EachBlog name="Urgent & Important" colorCode="#FF5733" />
|
<EachBlog name="Urgent & Not important" colorCode="#ffb000" icon={<FiClock />} contentList={contentList_uni} />
|
||||||
<EachBlog name="Urgent & Not important" colorCode="#FDDD5C" />
|
<EachBlog
|
||||||
<EachBlog name="Not urgent & Important" colorCode="#189AB4" />
|
name="Not urgent & Important"
|
||||||
<EachBlog name="Not urgent & Not important" colorCode="#94FA92" />
|
colorCode="#4772fa"
|
||||||
|
icon={<FiCheckCircle />}
|
||||||
|
contentList={contentList_nui}
|
||||||
|
/>
|
||||||
|
<EachBlog
|
||||||
|
name="Not urgent & Not important"
|
||||||
|
colorCode="#0cce9c"
|
||||||
|
icon={<FiXCircle />}
|
||||||
|
contentList={contentList_nuni}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from "react";
|
||||||
import { ApiUpdateUserProfile } from '../api/UserProfileApi';
|
import { ApiUpdateUserProfile } from "../api/UserProfileApi";
|
||||||
|
|
||||||
function ProfileUpdate() {
|
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 [fullName, setFullName] = useState("");
|
||||||
const [about, setAbout] = useState('');
|
const [about, setAbout] = useState("");
|
||||||
const defaultImage = 'https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg';
|
const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg";
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const handleImageUpload = () => {
|
const handleImageUpload = () => {
|
||||||
@ -15,7 +15,7 @@ function ProfileUpdate() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = e => {
|
||||||
const selectedFile = e.target.files[0];
|
const selectedFile = e.target.files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
@ -24,9 +24,9 @@ function ProfileUpdate() {
|
|||||||
|
|
||||||
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("first_name", username);
|
||||||
formData.append('about', about);
|
formData.append("about", about);
|
||||||
|
|
||||||
ApiUpdateUserProfile(formData);
|
ApiUpdateUserProfile(formData);
|
||||||
};
|
};
|
||||||
@ -45,10 +45,7 @@ function ProfileUpdate() {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div className="avatar w-32 h-32 cursor-pointer hover:blur" onClick={handleImageUpload}>
|
||||||
className="avatar w-32 h-32 cursor-pointer hover:blur"
|
|
||||||
onClick={handleImageUpload}
|
|
||||||
>
|
|
||||||
{file ? (
|
{file ? (
|
||||||
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
|
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
|
||||||
) : (
|
) : (
|
||||||
@ -69,7 +66,7 @@ function ProfileUpdate() {
|
|||||||
placeholder="Enter your username"
|
placeholder="Enter your username"
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -81,7 +78,7 @@ function ProfileUpdate() {
|
|||||||
placeholder="Enter your full name"
|
placeholder="Enter your full name"
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
value={fullName}
|
value={fullName}
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={e => setFullName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -92,7 +89,7 @@ function ProfileUpdate() {
|
|||||||
placeholder="Tell us about yourself"
|
placeholder="Tell us about yourself"
|
||||||
className="textarea w-full h-32"
|
className="textarea w-full h-32"
|
||||||
value={about}
|
value={about}
|
||||||
onChange={(e) => setAbout(e.target.value)}
|
onChange={e => setAbout(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,4 +101,4 @@ function ProfileUpdate() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfileUpdate;
|
export default ProfileUpdateComponent;
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
function IsAuthenticated() {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const access_token = localStorage.getItem('access_token');
|
|
||||||
|
|
||||||
if (access_token) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
} else {
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return isAuthenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IsAuthenticated;
|
|
||||||
@ -5,9 +5,13 @@ import { useGoogleLogin } from "@react-oauth/google";
|
|||||||
import refreshAccessToken from "./refreshAcesstoken";
|
import refreshAccessToken from "./refreshAcesstoken";
|
||||||
import axiosapi from "../../api/AuthenticationApi";
|
import axiosapi from "../../api/AuthenticationApi";
|
||||||
|
|
||||||
|
import { useAuth } from "../../hooks/authentication/IsAuthenticated";
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
|
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refreshAccessToken()) {
|
if (!refreshAccessToken()) {
|
||||||
Navigate("/");
|
Navigate("/");
|
||||||
@ -39,11 +43,13 @@ function LoginPage() {
|
|||||||
localStorage.setItem("access_token", res.data.access);
|
localStorage.setItem("access_token", res.data.access);
|
||||||
localStorage.setItem("refresh_token", res.data.refresh);
|
localStorage.setItem("refresh_token", res.data.refresh);
|
||||||
axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access;
|
axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access;
|
||||||
|
setIsAuthenticated(true);
|
||||||
Navigate("/");
|
Navigate("/");
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log("Login failed");
|
console.log("Login failed");
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
setIsAuthenticated(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,10 +64,12 @@ function LoginPage() {
|
|||||||
|
|
||||||
localStorage.setItem("access_token", access_token);
|
localStorage.setItem("access_token", access_token);
|
||||||
localStorage.setItem("refresh_token", refresh_token);
|
localStorage.setItem("refresh_token", refresh_token);
|
||||||
|
setIsAuthenticated(true);
|
||||||
Navigate("/");
|
Navigate("/");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error with the POST request:", error);
|
console.error("Error with the POST request:", error);
|
||||||
|
setIsAuthenticated(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: errorResponse => console.log(errorResponse),
|
onError: errorResponse => console.log(errorResponse),
|
||||||
|
|||||||
@ -1,31 +1,30 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import axiosapi from '../../api/AuthenticationApi';
|
import axiosapi from "../../api/AuthenticationApi";
|
||||||
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
|
||||||
import Link from '@mui/material/Link';
|
|
||||||
import Grid from '@mui/material/Grid';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Container from '@mui/material/Container';
|
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
|
||||||
|
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Container from "@mui/material/Container";
|
||||||
|
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||||
|
|
||||||
function Copyright(props) {
|
function Copyright(props) {
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
||||||
{'Copyright © '}
|
{"Copyright © "}
|
||||||
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
|
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
|
||||||
TurTask
|
TurTask
|
||||||
</Link>{' '}
|
</Link>{" "}
|
||||||
{new Date().getFullYear()}
|
{new Date().getFullYear()}
|
||||||
{'.'}
|
{"."}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -33,18 +32,17 @@ function Copyright(props) {
|
|||||||
const defaultTheme = createTheme();
|
const defaultTheme = createTheme();
|
||||||
|
|
||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
|
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: "",
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
});
|
});
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -52,15 +50,15 @@ export default function SignUp() {
|
|||||||
try {
|
try {
|
||||||
axiosapi.createUser(formData);
|
axiosapi.createUser(formData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating user:', error);
|
console.error("Error creating user:", error);
|
||||||
setError('Registration failed. Please try again.');
|
setError("Registration failed. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
Navigate('/login');
|
Navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = e => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData({ ...formData, [name]: value });
|
setFormData({ ...formData, [name]: value });
|
||||||
};
|
};
|
||||||
@ -72,12 +70,11 @@ export default function SignUp() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
}}
|
}}>
|
||||||
>
|
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
|
||||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
|
||||||
<LockOutlinedIcon />
|
<LockOutlinedIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
@ -127,12 +124,7 @@ export default function SignUp() {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button
|
<Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
|
||||||
type="submit"
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
sx={{ mt: 3, mb: 2 }}
|
|
||||||
>
|
|
||||||
Sign Up
|
Sign Up
|
||||||
</Button>
|
</Button>
|
||||||
<Grid container justifyContent="flex-end">
|
<Grid container justifyContent="flex-end">
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
|
|
||||||
async function refreshAccessToken() {
|
async function refreshAccessToken() {
|
||||||
const refresh_token = localStorage.getItem('refresh_token');
|
const refresh_token = localStorage.getItem("refresh_token");
|
||||||
const access_token = localStorage.getItem('access_token');
|
const access_token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
if (access_token) {
|
if (access_token) {
|
||||||
return true;
|
return true;
|
||||||
@ -12,7 +12,7 @@ async function refreshAccessToken() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshUrl = 'http://127.0.0.1:8000/api/token/refresh/';
|
const refreshUrl = "http://127.0.0.1:8000/api/token/refresh/";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(refreshUrl, { refresh: refresh_token });
|
const response = await axios.post(refreshUrl, { refresh: refresh_token });
|
||||||
@ -22,8 +22,8 @@ async function refreshAccessToken() {
|
|||||||
const newAccessToken = response.data.access;
|
const newAccessToken = response.data.access;
|
||||||
const newRefreshToken = response.data.refresh;
|
const newRefreshToken = response.data.refresh;
|
||||||
|
|
||||||
localStorage.setItem('access_token', newAccessToken);
|
localStorage.setItem("access_token", newAccessToken);
|
||||||
localStorage.setItem('refresh_token', newRefreshToken);
|
localStorage.setItem("refresh_token", newRefreshToken);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,35 +1,19 @@
|
|||||||
import { fetchTodoTasks } from '../../api/TaskApi';
|
import { readTodoTasks } from "../../api/TaskApi";
|
||||||
|
|
||||||
let eventGuid = 0
|
let eventGuid = 0;
|
||||||
|
|
||||||
// function getDateAndTime(dateString) {
|
const mapResponseToEvents = response => {
|
||||||
// const dateObject = new Date(dateString);
|
|
||||||
|
|
||||||
// const year = dateObject.getFullYear();
|
|
||||||
// const month = (dateObject.getMonth() + 1).toString().padStart(2, '0');
|
|
||||||
// const day = dateObject.getDate().toString().padStart(2, '0');
|
|
||||||
// const dateFormatted = `${year}-${month}-${day}`;
|
|
||||||
|
|
||||||
// const hours = dateObject.getUTCHours().toString().padStart(2, '0');
|
|
||||||
// const minutes = dateObject.getUTCMinutes().toString().padStart(2, '0');
|
|
||||||
// const seconds = dateObject.getUTCSeconds().toString().padStart(2, '0');
|
|
||||||
// const timeFormatted = `T${hours}:${minutes}:${seconds}`;
|
|
||||||
|
|
||||||
// return dateFormatted + timeFormatted;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const mapResponseToEvents = (response) => {
|
|
||||||
return response.map(item => ({
|
return response.map(item => ({
|
||||||
id: createEventId(),
|
id: createEventId(),
|
||||||
title: item.title,
|
title: item.title,
|
||||||
start: item.start_event,
|
start: item.start_event,
|
||||||
end: item.end_event,
|
end: item.end_event,
|
||||||
}));
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function getEvents() {
|
export async function getEvents() {
|
||||||
try {
|
try {
|
||||||
const response = await fetchTodoTasks();
|
const response = await readTodoTasks();
|
||||||
return mapResponseToEvents(response);
|
return mapResponseToEvents(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { formatDate } from "@fullcalendar/core";
|
import { formatDate } from "@fullcalendar/core";
|
||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
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 './index.css'
|
|
||||||
|
|
||||||
export default class Calendar extends React.Component {
|
export default class Calendar extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
@ -15,9 +14,9 @@ export default class Calendar extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="demo-app">
|
<div className="flex font-sans w-full h-screen">
|
||||||
{this.renderSidebar()}
|
{this.renderSidebar()}
|
||||||
<div className="demo-app-main">
|
<div className="flex-grow p-16 overflow-y-auto h-full max-h-screen">
|
||||||
<FullCalendar
|
<FullCalendar
|
||||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
headerToolbar={{
|
headerToolbar={{
|
||||||
@ -31,16 +30,11 @@ export default class Calendar extends React.Component {
|
|||||||
selectMirror={true}
|
selectMirror={true}
|
||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
weekends={this.state.weekendsVisible}
|
weekends={this.state.weekendsVisible}
|
||||||
initialEvents={getEvents} // alternatively, use the `events` setting to fetch from a feed
|
initialEvents={getEvents}
|
||||||
select={this.handleDateSelect}
|
select={this.handleDateSelect}
|
||||||
eventContent={renderEventContent} // custom render function
|
eventContent={renderEventContent}
|
||||||
eventClick={this.handleEventClick}
|
eventClick={this.handleEventClick}
|
||||||
eventsSet={this.handleEvents} // called after events are initialized/added/changed/removed
|
eventsSet={this.handleEvents}
|
||||||
/* you can update a remote database when these fire:
|
|
||||||
eventAdd={function(){}}
|
|
||||||
eventChange={function(){}}
|
|
||||||
eventRemove={function(){}}
|
|
||||||
*/
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,23 +43,30 @@ export default class Calendar extends React.Component {
|
|||||||
|
|
||||||
renderSidebar() {
|
renderSidebar() {
|
||||||
return (
|
return (
|
||||||
<div className="demo-app-sidebar">
|
<div className="w-72 bg-blue-100 border-r border-blue-200 p-8 flex-shrink-0">
|
||||||
<div className="demo-app-sidebar-section">
|
<div className="mb-8">
|
||||||
<h2>Instructions</h2>
|
<h2 className="text-xl font-bold">Instructions</h2>
|
||||||
<ul>
|
<ul className="list-disc pl-4">
|
||||||
<li>Select dates and you will be prompted to create a new event</li>
|
<li>Select dates and you will be prompted to create a new event</li>
|
||||||
<li>Drag, drop, and resize events</li>
|
<li>Drag, drop, and resize events</li>
|
||||||
<li>Click an event to delete it</li>
|
<li>Click an event to delete it</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="demo-app-sidebar-section">
|
|
||||||
<label>
|
<div className="mb-8">
|
||||||
<input type="checkbox" checked={this.state.weekendsVisible} onChange={this.handleWeekendsToggle}></input>
|
<label className="flex items-center">
|
||||||
toggle weekends
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.weekendsVisible}
|
||||||
|
onChange={this.handleWeekendsToggle}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
Toggle weekends
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="demo-app-sidebar-section">
|
|
||||||
<h2>All Events ({this.state.currentEvents.length})</h2>
|
<div>
|
||||||
|
<h2 className="text-xl font-bold">All Events ({this.state.currentEvents.length})</h2>
|
||||||
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
|
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
body > div { /* the react root */
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 1.5em 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
b { /* used for event dates/times */
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-app {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100%;
|
|
||||||
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-app-sidebar {
|
|
||||||
width: 300px;
|
|
||||||
line-height: 1.5;
|
|
||||||
background: #eaf9ff;
|
|
||||||
border-right: 1px solid #d3e2e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-app-sidebar-section {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-app-main {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc { /* the calendar root */
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
function PlusIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlusIcon;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
function TrashIcon() {
|
|
||||||
return (
|
|
||||||
React.createElement(
|
|
||||||
"svg",
|
|
||||||
{
|
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
|
||||||
fill: "none",
|
|
||||||
viewBox: "0 0 24 24",
|
|
||||||
strokeWidth: 1.5,
|
|
||||||
className: "w-6 h-6"
|
|
||||||
},
|
|
||||||
React.createElement("path", {
|
|
||||||
strokeLinecap: "round",
|
|
||||||
strokeLinejoin: "round",
|
|
||||||
d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TrashIcon;
|
|
||||||
@ -1,33 +1,18 @@
|
|||||||
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
import TrashIcon from "../icons/trashIcon";
|
import { BsFillTrashFill } from "react-icons/bs";
|
||||||
|
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import PlusIcon from "../icons/plusIcon";
|
|
||||||
import TaskCard from "./taskCard";
|
import TaskCard from "./taskCard";
|
||||||
|
|
||||||
function ColumnContainer({
|
function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
|
||||||
column,
|
|
||||||
deleteColumn,
|
|
||||||
updateColumn,
|
|
||||||
createTask,
|
|
||||||
tasks,
|
|
||||||
deleteTask,
|
|
||||||
updateTask,
|
|
||||||
}) {
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
const tasksIds = useMemo(() => {
|
const tasksIds = useMemo(() => {
|
||||||
return tasks.map((task) => task.id);
|
return tasks.map(task => task.id);
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
const {
|
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
||||||
setNodeRef,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({
|
|
||||||
id: column.id,
|
id: column.id,
|
||||||
data: {
|
data: {
|
||||||
type: "Column",
|
type: "Column",
|
||||||
@ -47,15 +32,15 @@ function ColumnContainer({
|
|||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className="
|
className="
|
||||||
bg-columnBackgroundColor
|
opacity-40
|
||||||
|
border-2
|
||||||
|
border-blue-500
|
||||||
w-[350px]
|
w-[350px]
|
||||||
h-[500px]
|
max-h-[400px]
|
||||||
max-h-[500px]
|
|
||||||
rounded-md
|
rounded-md
|
||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
"
|
"></div>
|
||||||
></div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,15 +49,13 @@ function ColumnContainer({
|
|||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className="
|
className="
|
||||||
bg-columnBackgroundColor
|
bg-[#f1f2f4]
|
||||||
w-[350px]
|
w-[280px]
|
||||||
h-[500px]
|
max-h-[400px]
|
||||||
max-h-[500px]
|
|
||||||
rounded-md
|
rounded-md
|
||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
"
|
">
|
||||||
>
|
|
||||||
{/* Column title */}
|
{/* Column title */}
|
||||||
<div
|
<div
|
||||||
{...attributes}
|
{...attributes}
|
||||||
@ -81,45 +64,26 @@ function ColumnContainer({
|
|||||||
setEditMode(true);
|
setEditMode(true);
|
||||||
}}
|
}}
|
||||||
className="
|
className="
|
||||||
bg-mainBackgroundColor
|
ml-3
|
||||||
text-md
|
text-md
|
||||||
h-[60px]
|
|
||||||
cursor-grab
|
cursor-grab
|
||||||
rounded-md
|
|
||||||
rounded-b-none
|
|
||||||
p-3
|
|
||||||
font-bold
|
font-bold
|
||||||
border-columnBackgroundColor
|
|
||||||
border-4
|
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-between
|
justify-between
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div
|
|
||||||
className="
|
|
||||||
flex
|
|
||||||
justify-center
|
|
||||||
items-center
|
|
||||||
bg-columnBackgroundColor
|
|
||||||
px-2
|
|
||||||
py-1
|
|
||||||
text-sm
|
|
||||||
rounded-full
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
{!editMode && column.title}
|
{!editMode && column.title}
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<input
|
<input
|
||||||
className="bg-white focus:border-rose-500 border rounded outline-none px-2"
|
className="bg-gray-200 focus:border-blue-500 border rounded-md outline-none px-2"
|
||||||
value={column.title}
|
value={column.title}
|
||||||
onChange={(e) => updateColumn(column.id, e.target.value)}
|
onChange={e => updateColumn(column.id, e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={e => {
|
||||||
if (e.key !== "Enter") return;
|
if (e.key !== "Enter") return;
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
}}
|
}}
|
||||||
@ -137,33 +101,26 @@ function ColumnContainer({
|
|||||||
rounded
|
rounded
|
||||||
px-1
|
px-1
|
||||||
py-2
|
py-2
|
||||||
"
|
">
|
||||||
>
|
<BsFillTrashFill />
|
||||||
<TrashIcon />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column task container */}
|
{/* Column task container */}
|
||||||
<div className="flex flex-grow flex-col gap-4 p-2 overflow-x-hidden overflow-y-auto">
|
<div className="flex flex-grow flex-col gap-2 p-1 overflow-x-hidden overflow-y-auto">
|
||||||
<SortableContext items={tasksIds}>
|
<SortableContext items={tasksIds}>
|
||||||
{tasks.map((task) => (
|
{tasks.map(task => (
|
||||||
<TaskCard
|
<TaskCard key={task.id} task={task} deleteTask={deleteTask} updateTask={updateTask} />
|
||||||
key={task.id}
|
|
||||||
task={task}
|
|
||||||
deleteTask={deleteTask} // Pass deleteTask to TaskCard
|
|
||||||
updateTask={updateTask}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
{/* Column footer */}
|
{/* Column footer */}
|
||||||
<button
|
<button
|
||||||
className="flex gap-2 items-center border-columnBackgroundColor border-2 rounded-md p-4 border-x-columnBackgroundColor hover:bg-mainBackgroundColor hover:text-rose-500 active:bg-white"
|
className="flex gap-2 items-center rounded-md p-2 my-2 hover:bg-zinc-200 active:bg-zinc-400"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createTask(column.id);
|
createTask(column.id);
|
||||||
}}
|
}}>
|
||||||
>
|
<AiOutlinePlusCircle />
|
||||||
<PlusIcon />
|
|
||||||
Add task
|
Add task
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import ColumnContainer from "./columnContainer";
|
||||||
|
|
||||||
|
function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
|
||||||
|
return (
|
||||||
|
<div className="card bg-[#f1f2f4] shadow p-1 my-2 border-2">
|
||||||
|
<ColumnContainer
|
||||||
|
column={column}
|
||||||
|
deleteColumn={deleteColumn}
|
||||||
|
updateColumn={updateColumn}
|
||||||
|
createTask={createTask}
|
||||||
|
tasks={tasks}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
updateTask={updateTask}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColumnContainerCard;
|
||||||
@ -1,16 +1,10 @@
|
|||||||
import PlusIcon from "../icons/plusIcon";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import ColumnContainer from "./columnContainer";
|
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";
|
||||||
|
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||||
|
|
||||||
const defaultCols = [
|
const defaultCols = [
|
||||||
{
|
{
|
||||||
@ -98,7 +92,7 @@ const defaultTasks = [
|
|||||||
|
|
||||||
function KanbanBoard() {
|
function KanbanBoard() {
|
||||||
const [columns, setColumns] = useState(defaultCols);
|
const [columns, setColumns] = useState(defaultCols);
|
||||||
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
|
const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
|
||||||
|
|
||||||
const [tasks, setTasks] = useState(defaultTasks);
|
const [tasks, setTasks] = useState(defaultTasks);
|
||||||
|
|
||||||
@ -123,19 +117,13 @@ function KanbanBoard() {
|
|||||||
items-center
|
items-center
|
||||||
overflow-x-auto
|
overflow-x-auto
|
||||||
overflow-y-hidden
|
overflow-y-hidden
|
||||||
"
|
">
|
||||||
>
|
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
|
||||||
<DndContext
|
<div className="ml-2 flex gap-4">
|
||||||
sensors={sensors}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onDragOver={onDragOver}
|
|
||||||
>
|
|
||||||
<div className="m-auto flex gap-4">
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<SortableContext items={columnsId}>
|
<SortableContext items={columnsId}>
|
||||||
{columns.map((col) => (
|
{columns.map(col => (
|
||||||
<ColumnContainer
|
<ColumnContainerCard
|
||||||
key={col.id}
|
key={col.id}
|
||||||
column={col}
|
column={col}
|
||||||
deleteColumn={deleteColumn}
|
deleteColumn={deleteColumn}
|
||||||
@ -143,7 +131,7 @@ function KanbanBoard() {
|
|||||||
createTask={createTask}
|
createTask={createTask}
|
||||||
deleteTask={deleteTask}
|
deleteTask={deleteTask}
|
||||||
updateTask={updateTask}
|
updateTask={updateTask}
|
||||||
tasks={tasks.filter((task) => task.columnId === col.id)}
|
tasks={tasks.filter(task => task.columnId === col.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
@ -155,47 +143,40 @@ function KanbanBoard() {
|
|||||||
}}
|
}}
|
||||||
className="
|
className="
|
||||||
h-[60px]
|
h-[60px]
|
||||||
w-[350px]
|
w-[268px]
|
||||||
min-w-[350px]
|
max-w-[268px]
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
rounded-lg
|
rounded-xl
|
||||||
bg-mainBackgroundColor
|
bg-[#f1f2f4]
|
||||||
border-2
|
border-2
|
||||||
border-columnBackgroundColor
|
|
||||||
p-4
|
p-4
|
||||||
ring-rose-500
|
hover:bg-gray-200
|
||||||
hover:ring-2
|
|
||||||
flex
|
flex
|
||||||
gap-2
|
gap-2
|
||||||
"
|
my-2
|
||||||
>
|
bg-opacity-60
|
||||||
<PlusIcon />
|
">
|
||||||
|
<div className="my-1">
|
||||||
|
<AiOutlinePlusCircle />
|
||||||
|
</div>
|
||||||
Add Column
|
Add Column
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<DragOverlay>
|
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
|
||||||
{activeColumn && (
|
{activeColumn && (
|
||||||
<ColumnContainer
|
<ColumnContainerCard
|
||||||
column={activeColumn}
|
column={activeColumn}
|
||||||
deleteColumn={deleteColumn}
|
deleteColumn={deleteColumn}
|
||||||
updateColumn={updateColumn}
|
updateColumn={updateColumn}
|
||||||
createTask={createTask}
|
createTask={createTask}
|
||||||
deleteTask={deleteTask}
|
deleteTask={deleteTask}
|
||||||
updateTask={updateTask}
|
updateTask={updateTask}
|
||||||
tasks={tasks.filter(
|
tasks={tasks.filter(task => task.columnId === activeColumn.id)}
|
||||||
(task) => task.columnId === activeColumn.id
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTask && (
|
|
||||||
<TaskCard
|
|
||||||
task={activeTask}
|
|
||||||
deleteTask={deleteTask}
|
|
||||||
updateTask={updateTask}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeTask && <TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />}
|
||||||
</DragOverlay>,
|
</DragOverlay>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
@ -214,12 +195,12 @@ function KanbanBoard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteTask(id) {
|
function deleteTask(id) {
|
||||||
const newTasks = tasks.filter((task) => task.id !== id);
|
const newTasks = tasks.filter(task => task.id !== id);
|
||||||
setTasks(newTasks);
|
setTasks(newTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTask(id, content) {
|
function updateTask(id, content) {
|
||||||
const newTasks = tasks.map((task) => {
|
const newTasks = tasks.map(task => {
|
||||||
if (task.id !== id) return task;
|
if (task.id !== id) return task;
|
||||||
return { ...task, content };
|
return { ...task, content };
|
||||||
});
|
});
|
||||||
@ -237,15 +218,15 @@ function KanbanBoard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteColumn(id) {
|
function deleteColumn(id) {
|
||||||
const filteredColumns = columns.filter((col) => col.id !== id);
|
const filteredColumns = columns.filter(col => col.id !== id);
|
||||||
setColumns(filteredColumns);
|
setColumns(filteredColumns);
|
||||||
|
|
||||||
const newTasks = tasks.filter((t) => t.columnId !== id);
|
const newTasks = tasks.filter(t => t.columnId !== id);
|
||||||
setTasks(newTasks);
|
setTasks(newTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateColumn(id, title) {
|
function updateColumn(id, title) {
|
||||||
const newColumns = columns.map((col) => {
|
const newColumns = columns.map(col => {
|
||||||
if (col.id !== id) return col;
|
if (col.id !== id) return col;
|
||||||
return { ...col, title };
|
return { ...col, title };
|
||||||
});
|
});
|
||||||
@ -280,10 +261,10 @@ function KanbanBoard() {
|
|||||||
const isActiveAColumn = active.data.current?.type === "Column";
|
const isActiveAColumn = active.data.current?.type === "Column";
|
||||||
if (!isActiveAColumn) return;
|
if (!isActiveAColumn) return;
|
||||||
|
|
||||||
setColumns((columns) => {
|
setColumns(columns => {
|
||||||
const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
|
const activeColumnIndex = columns.findIndex(col => col.id === activeId);
|
||||||
|
|
||||||
const overColumnIndex = columns.findIndex((col) => col.id === overId);
|
const overColumnIndex = columns.findIndex(col => col.id === overId);
|
||||||
|
|
||||||
return arrayMove(columns, activeColumnIndex, overColumnIndex);
|
return arrayMove(columns, activeColumnIndex, overColumnIndex);
|
||||||
});
|
});
|
||||||
@ -304,9 +285,9 @@ function KanbanBoard() {
|
|||||||
if (!isActiveATask) return;
|
if (!isActiveATask) return;
|
||||||
|
|
||||||
if (isActiveATask && isOverATask) {
|
if (isActiveATask && isOverATask) {
|
||||||
setTasks((tasks) => {
|
setTasks(tasks => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
const activeIndex = tasks.findIndex(t => t.id === activeId);
|
||||||
const overIndex = tasks.findIndex((t) => t.id === overId);
|
const overIndex = tasks.findIndex(t => t.id === overId);
|
||||||
|
|
||||||
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
|
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
|
||||||
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
||||||
@ -320,8 +301,8 @@ function KanbanBoard() {
|
|||||||
const isOverAColumn = over.data.current?.type === "Column";
|
const isOverAColumn = over.data.current?.type === "Column";
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
tasks[activeIndex].columnId = overId;
|
tasks[activeIndex].columnId = overId;
|
||||||
return arrayMove(tasks, activeIndex, activeIndex);
|
return arrayMove(tasks, activeIndex, activeIndex);
|
||||||
|
|||||||
36
frontend/src/components/kanbanBoard/kanbanPage.jsx
Normal file
36
frontend/src/components/kanbanBoard/kanbanPage.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import KanbanBoard from "./kanbanBoard";
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const KanbanPage = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('kanban');
|
||||||
|
|
||||||
|
const handleTabClick = (tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex justify-center border-2 py-3 mb-1">
|
||||||
|
<div>
|
||||||
|
<div className="tabs tabs-boxed">
|
||||||
|
<a
|
||||||
|
id="kanban"
|
||||||
|
className={`tab ${activeTab === "kanban" ? "tab-active" : ""}`}
|
||||||
|
onClick={() => handleTabClick("kanban")}>
|
||||||
|
Kanban
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id="table"
|
||||||
|
className={`tab ${activeTab === "table" ? "tab-active" : ""}`}
|
||||||
|
onClick={() => handleTabClick("table")}>
|
||||||
|
Table
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<KanbanBoard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KanbanPage;
|
||||||
@ -1,26 +1,18 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import TrashIcon from "../icons/trashIcon";
|
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";
|
||||||
|
|
||||||
function TaskCard({ task, deleteTask, updateTask }) {
|
function TaskCard({ task, deleteTask, updateTask }) {
|
||||||
const [mouseIsOver, setMouseIsOver] = useState(false);
|
const [mouseIsOver, setMouseIsOver] = useState(false);
|
||||||
const [editMode, setEditMode] = useState(true);
|
|
||||||
|
|
||||||
const {
|
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
||||||
setNodeRef,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({
|
|
||||||
id: task.id,
|
id: task.id,
|
||||||
data: {
|
data: {
|
||||||
type: "Task",
|
type: "Task",
|
||||||
task,
|
task,
|
||||||
},
|
},
|
||||||
disabled: editMode,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
@ -28,11 +20,9 @@ function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleEditMode = () => {
|
{
|
||||||
setEditMode((prev) => !prev);
|
/* If card is dragged */
|
||||||
setMouseIsOver(false);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -40,57 +30,30 @@ function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
style={style}
|
style={style}
|
||||||
className="
|
className="
|
||||||
opacity-30
|
opacity-30
|
||||||
bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl border-2 border-rose-500 cursor-grab relative
|
bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editMode) {
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<TaskDetailModal />
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
className="outline-double outline-1 outline-offset-2 hover:outline-double hover:outline-1 hover:outline-offset-2 bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
className="
|
|
||||||
h-[90%]
|
|
||||||
w-full resize-none border-none rounded bg-transparent text-black focus:outline-none
|
|
||||||
"
|
|
||||||
value={task.content}
|
|
||||||
autoFocus
|
|
||||||
placeholder="Task content here"
|
|
||||||
onBlur={toggleEditMode}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && e.shiftKey) {
|
|
||||||
toggleEditMode();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e) => updateTask(task.id, e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
style={style}
|
||||||
{...attributes}
|
className="justify-center items-center flex text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
|
||||||
{...listeners}
|
|
||||||
onClick={toggleEditMode}
|
|
||||||
className="outline-double outline-1 outline-offset-2 hover:outline-double hover:outline-1 hover:outline-offset-2 sbg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative task"
|
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setMouseIsOver(true);
|
setMouseIsOver(true);
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setMouseIsOver(false);
|
setMouseIsOver(false);
|
||||||
}}
|
}}>
|
||||||
>
|
<p
|
||||||
<p className="my-auto h-[90%] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap">
|
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").showModal()}>
|
||||||
{task.content}
|
{task.content}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -99,12 +62,12 @@ function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteTask(task.id);
|
deleteTask(task.id);
|
||||||
}}
|
}}
|
||||||
className="stroke-white absolute right-4 top-1/2 -translate-y-1/2 bg-columnBackgroundColor p-2 rounded opacity-60 hover:opacity-100"
|
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 />
|
||||||
<TrashIcon />
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
frontend/src/components/kanbanBoard/taskDetailModal.jsx
Normal file
17
frontend/src/components/kanbanBoard/taskDetailModal.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function TaskDetailModal() {
|
||||||
|
return (
|
||||||
|
<dialog id="task_detail_modal" className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<form method="dialog">
|
||||||
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
</form>
|
||||||
|
<h3 className="font-bold text-lg">Hello!</h3>
|
||||||
|
<p className="py-4">Press ESC key or click on ✕ button to close</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskDetailModal;
|
||||||
@ -1,11 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList, AiOutlinePieChart } from "react-icons/ai";
|
||||||
AiOutlineHome,
|
import { PiStepsDuotone } from "react-icons/pi";
|
||||||
AiOutlineSchedule,
|
|
||||||
AiOutlineUnorderedList,
|
|
||||||
AiOutlinePieChart,
|
|
||||||
AiOutlinePlus,
|
|
||||||
} from "react-icons/ai";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@ -14,7 +9,7 @@ const menuItems = [
|
|||||||
{ id: 1, path: "/tasks", icon: <AiOutlineUnorderedList /> },
|
{ id: 1, path: "/tasks", icon: <AiOutlineUnorderedList /> },
|
||||||
{ id: 2, path: "/calendar", icon: <AiOutlineSchedule /> },
|
{ id: 2, path: "/calendar", icon: <AiOutlineSchedule /> },
|
||||||
{ id: 3, path: "/analytic", icon: <AiOutlinePieChart /> },
|
{ id: 3, path: "/analytic", icon: <AiOutlinePieChart /> },
|
||||||
{ id: 4, path: "/priority", icon: <AiOutlinePlus /> },
|
{ id: 4, path: "/priority", icon: <PiStepsDuotone /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const IconSideNav = () => {
|
const IconSideNav = () => {
|
||||||
@ -29,7 +24,7 @@ const SideNav = () => {
|
|||||||
const [selected, setSelected] = useState(0);
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-screen">
|
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-full fixed top-0 left-0 z-50">
|
||||||
{menuItems.map(item => (
|
{menuItems.map(item => (
|
||||||
<NavItem
|
<NavItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
70
frontend/src/components/navigations/Navbar.jsx
Normal file
70
frontend/src/components/navigations/Navbar.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import axiosapi from "../../api/AuthenticationApi";
|
||||||
|
import { useAuth } from "../../hooks/authentication/IsAuthenticated";
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
Profile: '/profile',
|
||||||
|
Account: '/account',
|
||||||
|
};
|
||||||
|
|
||||||
|
function NavBar() {
|
||||||
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
|
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
axiosapi.apiUserLogout();
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
Navigate("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-theme="night" className="navbar bg-base-100">
|
||||||
|
<div className="flex-1">
|
||||||
|
<a className="btn btn-ghost normal-case text-xl" href="/">
|
||||||
|
TurTask
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none gap-2">
|
||||||
|
<div className="form-control">
|
||||||
|
<input type="text" placeholder="Search" className="input input-bordered w-24 md:w-auto" />
|
||||||
|
</div>
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
||||||
|
<div className="w-10 rounded-full">
|
||||||
|
<img src="https://us-tuna-sounds-images.voicemod.net/f322631f-689a-43ac-81ab-17a70f27c443-1692187175560.png" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<a href={settings.Profile} className="justify-between">
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={settings.Account}>Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a onClick={logout}>Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="btn btn-outline btn-info" onClick={() => Navigate("/login")}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-success" onClick={() => Navigate("/signup")}>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default NavBar;
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import IsAuthenticated from "../authentication/IsAuthenticated";
|
|
||||||
import axiosapi from "../../api/AuthenticationApi";
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
Profile: '/profile',
|
|
||||||
Account: '/account',
|
|
||||||
};
|
|
||||||
|
|
||||||
function NavBar() {
|
|
||||||
const Navigate = useNavigate();
|
|
||||||
|
|
||||||
const isAuthenticated = IsAuthenticated();
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
axiosapi.apiUserLogout();
|
|
||||||
Navigate("/");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-theme="night" className="navbar bg-base-100">
|
|
||||||
<div className="flex-1">
|
|
||||||
<a className="btn btn-ghost normal-case text-xl" href="/">
|
|
||||||
TurTask
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex-none gap-2">
|
|
||||||
<div className="form-control">
|
|
||||||
<input type="text" placeholder="Search" className="input input-bordered w-24 md:w-auto" />
|
|
||||||
</div>
|
|
||||||
{isAuthenticated ? (
|
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
|
||||||
<div className="w-10 rounded-full">
|
|
||||||
<img src="https://us-tuna-sounds-images.voicemod.net/f322631f-689a-43ac-81ab-17a70f27c443-1692187175560.png" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
|
|
||||||
<li>
|
|
||||||
<a href={settings.Profile} className="justify-between">
|
|
||||||
Profile
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={settings.Account}>Settings</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a onClick={logout}>Logout</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button className="btn btn-outline btn-info" onClick={() => Navigate("/login")}>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-success" onClick={() => Navigate("/signup")}>
|
|
||||||
Sign Up
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default NavBar;
|
|
||||||
146
frontend/src/components/profilePage.jsx
Normal file
146
frontend/src/components/profilePage.jsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import ProfileUpdateComponent from "./ProfileUpdateComponent";
|
||||||
|
|
||||||
|
function ProfileUpdatePage() {
|
||||||
|
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-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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-title truncate">Level</div>
|
||||||
|
<div className="stat-value flex">
|
||||||
|
1
|
||||||
|
<div className="stat-figure text-secondary px-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="#3abff8"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
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 class="progress progress-info w-36" value="10" max="100"></progress>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-title">Gold</div>
|
||||||
|
<div className="stat-value flex truncate">
|
||||||
|
331412421
|
||||||
|
<div className="stat-figure text-secondary px-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">About me</h2>
|
||||||
|
<div class="card-actions justify-end"></div>
|
||||||
|
<textarea class="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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 class="card-body">
|
||||||
|
<h2 class="card-title">Overall Statistics</h2>
|
||||||
|
<div class="card-actions justify-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-start-2 row-start-2">
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Achievements</h2>
|
||||||
|
<div class="card-actions justify-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-start-1 row-start-2">
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Friends</h2>
|
||||||
|
<div class="card-actions justify-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixed bottom-4 right-4">
|
||||||
|
<ul className="menu menu-horizontal bg-base-200 rounded-box">
|
||||||
|
<li>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<dialog id="my_modal_4" className="modal">
|
||||||
|
<div className="modal-box w-11/12 max-w-5xl flex flex-col">
|
||||||
|
<form method="dialog">
|
||||||
|
<ProfileUpdateComponent />
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default ProfileUpdatePage;
|
||||||
@ -1,26 +1,26 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import axiosapi from '../api/axiosapi';
|
import axiosapi from "../api/axiosapi";
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from "@material-ui/core/TextField";
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from "@material-ui/core/Typography";
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||||
import Container from '@material-ui/core/Container';
|
import Container from "@material-ui/core/Container";
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from "@material-ui/core/Button";
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
// Styles for various elements
|
// Styles for various elements
|
||||||
paper: {
|
paper: {
|
||||||
marginTop: theme.spacing(8),
|
marginTop: theme.spacing(8),
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
margin: theme.spacing(1),
|
margin: theme.spacing(1),
|
||||||
backgroundColor: theme.palette.secondary.main,
|
backgroundColor: theme.palette.secondary.main,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
@ -31,14 +31,14 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
const Signup = () => {
|
const Signup = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: "",
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
});
|
});
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -46,14 +46,14 @@ const Signup = () => {
|
|||||||
try {
|
try {
|
||||||
axiosapi.createUser(formData);
|
axiosapi.createUser(formData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating user:', error);
|
console.error("Error creating user:", error);
|
||||||
setError('Registration failed. Please try again.'); // Set an error message
|
setError("Registration failed. Please try again."); // Set an error message
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = e => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData({ ...formData, [name]: value });
|
setFormData({ ...formData, [name]: value });
|
||||||
};
|
};
|
||||||
@ -102,9 +102,8 @@ const Signup = () => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
className={classes.submit}
|
className={classes.submit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}>
|
||||||
>
|
{isSubmitting ? "Signing up..." : "Sign Up"}
|
||||||
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{error && <Typography color="error">{error}</Typography>}
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import axiosapi from '../api/AuthenticationApi';
|
import axiosapi from "../api/AuthenticationApi";
|
||||||
import { Button } from '@mui/material';
|
import { Button } from "@mui/material";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function TestAuth() {
|
function TestAuth() {
|
||||||
let Navigate = useNavigate();
|
let Navigate = useNavigate();
|
||||||
@ -10,10 +10,13 @@ function TestAuth() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch the "hello" data from the server when the component mounts
|
// Fetch the "hello" data from the server when the component mounts
|
||||||
axiosapi.getGreeting().then(res => {
|
axiosapi
|
||||||
|
.getGreeting()
|
||||||
|
.then(res => {
|
||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
setMessage(res.data.user);
|
setMessage(res.data.user);
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch(err => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setMessage("");
|
setMessage("");
|
||||||
});
|
});
|
||||||
@ -22,8 +25,8 @@ function TestAuth() {
|
|||||||
const logout = () => {
|
const logout = () => {
|
||||||
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
||||||
axiosapi.apiUserLogout();
|
axiosapi.apiUserLogout();
|
||||||
Navigate('/testAuth');
|
Navigate("/testAuth");
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -31,7 +34,9 @@ function TestAuth() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold">Login! Hello!</h1>
|
<h1 class="text-xl font-bold">Login! Hello!</h1>
|
||||||
<h2>{message}</h2>
|
<h2>{message}</h2>
|
||||||
<Button variant="contained" onClick={logout}>Logout</Button>
|
<Button variant="contained" onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message === "" && <h1 class="text-xl font-bold">Need to sign in, No authentication found</h1>}
|
{message === "" && <h1 class="text-xl font-bold">Need to sign in, No authentication found</h1>}
|
||||||
|
|||||||
39
frontend/src/hooks/authentication/IsAuthenticated.jsx
Normal file
39
frontend/src/hooks/authentication/IsAuthenticated.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const AuthContext = createContext();
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||||||
|
const access_token = localStorage.getItem("access_token");
|
||||||
|
return !!access_token;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleTokenChange = () => {
|
||||||
|
const newAccessToken = localStorage.getItem("access_token");
|
||||||
|
setIsAuthenticated(!!newAccessToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTokenChange();
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleTokenChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", handleTokenChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -8,16 +8,14 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
import React from "react";
|
import React, { Fragment } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { GoogleOAuthProvider} from '@react-oauth/google';
|
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { AuthProvider } from "./hooks/authentication/IsAuthenticated";
|
||||||
|
|
||||||
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID
|
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<Fragment>
|
||||||
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</AuthProvider>
|
||||||
|
</Fragment>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</GoogleOAuthProvider>
|
</GoogleOAuthProvider>
|
||||||
);
|
);
|
||||||
@ -15,3 +15,4 @@ google-auth-httplib2>=0.1
|
|||||||
django-storages[s3]>=1.14
|
django-storages[s3]>=1.14
|
||||||
Pillow>=10.1
|
Pillow>=10.1
|
||||||
drf-spectacular>=0.26
|
drf-spectacular>=0.26
|
||||||
|
python-dateutil>=2.8
|
||||||
Loading…
Reference in New Issue
Block a user