Merge branch 'main' into feature/kanban-board

This commit is contained in:
THIS ONE IS A LITTLE BIT TRICKY KRUB 2023-11-18 21:40:39 +07:00
commit e19f60f542
45 changed files with 1183 additions and 793 deletions

View File

@ -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)

View 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,
},
),
]

View File

@ -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(),
),
]

View File

@ -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.

View File

@ -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)

View File

@ -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',)

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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)]),
),
]

View File

@ -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>

View 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;

View File

@ -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;
});
};

View File

@ -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);

View File

@ -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;
} }
}; };

View File

@ -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;

View File

@ -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>
); );

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
function HomePage() { function HomePage() {
return ( return (

View File

@ -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;

View File

@ -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;

View File

@ -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),

View File

@ -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">

View File

@ -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 {

View File

@ -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);

View File

@ -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>

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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);

View 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;

View File

@ -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>
); );
} }

View 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;

View File

@ -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}

View 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;

View File

@ -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;

View 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;

View File

@ -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>}

View File

@ -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>}

View 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;
};

View File

@ -8,20 +8,18 @@
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 {
color:black; color: black;
/* border: 1px solid white; */ /* border: 1px solid white; */
padding: 1em; padding: 1em;
} }

View File

@ -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>
); );

View File

@ -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