mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 22:14:07 +01:00
Merge branch 'main' into dashboard-api
This commit is contained in:
commit
02e82e85bc
0
backend/boards/__init__.py
Normal file
0
backend/boards/__init__.py
Normal file
11
backend/boards/admin.py
Normal file
11
backend/boards/admin.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Board, ListBoard
|
||||||
|
|
||||||
|
@admin.register(Board)
|
||||||
|
class BoardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'user']
|
||||||
|
|
||||||
|
@admin.register(ListBoard)
|
||||||
|
class ListBoardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'position', 'board']
|
||||||
|
list_filter = ['board', 'position']
|
||||||
9
backend/boards/apps.py
Normal file
9
backend/boards/apps.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BoardsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'boards'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import boards.signals
|
||||||
35
backend/boards/migrations/0001_initial.py
Normal file
35
backend/boards/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-19 19:19
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Board',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ListBoard',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('position', models.IntegerField()),
|
||||||
|
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boards.board')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/boards/migrations/__init__.py
Normal file
0
backend/boards/migrations/__init__.py
Normal file
34
backend/boards/models.py
Normal file
34
backend/boards/models.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
class Board(models.Model):
|
||||||
|
"""
|
||||||
|
Kanban board model.
|
||||||
|
|
||||||
|
:param user: The user who owns the board.
|
||||||
|
:param name: The name of the board.
|
||||||
|
:param created_at: The date and time when the board was created.
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class ListBoard(models.Model):
|
||||||
|
"""
|
||||||
|
List inside a Kanban board.
|
||||||
|
|
||||||
|
:param board: The board that the list belongs to.
|
||||||
|
:param name: The name of the list.
|
||||||
|
:param position: The position of the list in Kanban.
|
||||||
|
"""
|
||||||
|
board = models.ForeignKey(Board, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
position = models.IntegerField()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}"
|
||||||
14
backend/boards/signals.py
Normal file
14
backend/boards/signals.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from boards.models import Board, ListBoard
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
@receiver(post_save, sender=CustomUser)
|
||||||
|
def create_default_board(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
board = Board.objects.create(user=instance, name="My Default Board")
|
||||||
|
|
||||||
|
ListBoard.objects.create(board=board, name="Todo", position=1)
|
||||||
|
ListBoard.objects.create(board=board, name="In Progress", position=2)
|
||||||
|
ListBoard.objects.create(board=board, name="Done", position=3)
|
||||||
3
backend/boards/tests.py
Normal file
3
backend/boards/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
5
backend/boards/urls.py
Normal file
5
backend/boards/urls.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
]
|
||||||
3
backend/boards/views.py
Normal file
3
backend/boards/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@ -53,6 +53,7 @@ INSTALLED_APPS = [
|
|||||||
'users',
|
'users',
|
||||||
'authentications',
|
'authentications',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
|
'boards',
|
||||||
|
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
|
|||||||
@ -28,4 +28,5 @@ urlpatterns = [
|
|||||||
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||||
path('api/', include('dashboard.urls')),
|
path('api/', include('dashboard.urls')),
|
||||||
|
path('api/', include('boards.urls')),
|
||||||
]
|
]
|
||||||
@ -1,3 +1,29 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Tag, Todo, RecurrenceTask, RecurrencePattern, Habit, Subtask
|
||||||
|
|
||||||
# Register your models here.
|
@admin.register(Tag)
|
||||||
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name']
|
||||||
|
|
||||||
|
@admin.register(Todo)
|
||||||
|
class TodoAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['title', 'list_board', 'is_active', 'priority']
|
||||||
|
list_filter = ['list_board', 'is_active', 'priority']
|
||||||
|
|
||||||
|
@admin.register(RecurrenceTask)
|
||||||
|
class RecurrenceTaskAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['title', 'list_board', 'rrule', 'is_active']
|
||||||
|
list_filter = ['list_board', 'rrule', 'is_active']
|
||||||
|
|
||||||
|
@admin.register(RecurrencePattern)
|
||||||
|
class RecurrencePatternAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['recurrence_task', 'recurring_type', 'day_of_week', 'week_of_month', 'day_of_month', 'month_of_year']
|
||||||
|
|
||||||
|
@admin.register(Habit)
|
||||||
|
class HabitAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['title', 'streak', 'current_count']
|
||||||
|
|
||||||
|
@admin.register(Subtask)
|
||||||
|
class SubtaskAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['parent_task', 'description', 'completed']
|
||||||
|
list_filter = ['parent_task', 'completed']
|
||||||
|
|||||||
@ -6,9 +6,9 @@ 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, generate_recurrence_rule
|
from tasks.utils import get_service
|
||||||
from tasks.models import Todo, RecurrenceTask
|
from tasks.models import Todo
|
||||||
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
|
from tasks.serializers import TodoUpdateSerializer
|
||||||
|
|
||||||
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
class GoogleCalendarEventViewset(viewsets.ViewSet):
|
||||||
"""Viewset for list or save Google Calendar Events."""
|
"""Viewset for list or save Google Calendar Events."""
|
||||||
@ -50,7 +50,11 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
return events
|
return events
|
||||||
|
|
||||||
def _validate_serializer(self, serializer):
|
def _validate_serializer(self, serializer):
|
||||||
"""Validate serializer and return response."""
|
"""
|
||||||
|
Validate serializer and return response.
|
||||||
|
|
||||||
|
:param serializer: The serializer to validate.
|
||||||
|
"""
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response("Validate Successfully", status=200)
|
return Response("Validate Successfully", status=200)
|
||||||
@ -61,7 +65,6 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
events = self._get_google_events(request)
|
events = self._get_google_events(request)
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
recurrence_task_ids = []
|
|
||||||
for event in events:
|
for event in events:
|
||||||
start_datetime = event.get('start', {}).get('dateTime')
|
start_datetime = event.get('start', {}).get('dateTime')
|
||||||
end_datetime = event.get('end', {}).get('dateTime')
|
end_datetime = event.get('end', {}).get('dateTime')
|
||||||
@ -71,25 +74,6 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
|
|||||||
event.pop('start')
|
event.pop('start')
|
||||||
event.pop('end')
|
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:
|
try:
|
||||||
task = Todo.objects.get(google_calendar_id=event['id'])
|
task = Todo.objects.get(google_calendar_id=event['id'])
|
||||||
serializer = TodoUpdateSerializer(instance=task, data=event)
|
serializer = TodoUpdateSerializer(instance=task, data=event)
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-19 20:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('boards', '0001_initial'),
|
||||||
|
('tasks', '0014_recurrencetask_completed_todo_completed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecurrencePattern',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('recurring_type', models.IntegerField(choices=[(0, 'Daily'), (1, 'Weekly'), (2, 'Monthly'), (3, 'Yearly')])),
|
||||||
|
('max_occurrences', models.IntegerField(default=0)),
|
||||||
|
('day_of_week', models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])),
|
||||||
|
('week_of_month', models.IntegerField(choices=[(1, 'First'), (2, 'Second'), (3, 'Third'), (4, 'Fourth'), (5, 'Last')])),
|
||||||
|
('day_of_month', models.IntegerField(default=0)),
|
||||||
|
('month_of_year', models.IntegerField(choices=[(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='UserNotification',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='habit',
|
||||||
|
name='end_event',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='habit',
|
||||||
|
name='google_calendar_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='habit',
|
||||||
|
name='start_event',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='google_calendar_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='recurrence_rule',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='habit',
|
||||||
|
name='current_count',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='is_full_day_event',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='parent_task',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='rrule',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='todo',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='todo',
|
||||||
|
name='is_full_day_event',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='todo',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Transaction',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrencepattern',
|
||||||
|
name='recurrence_task',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-19 20:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('boards', '0001_initial'),
|
||||||
|
('tasks', '0015_recurrencepattern_remove_transaction_user_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='todo',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-19 20:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('boards', '0001_initial'),
|
||||||
|
('tasks', '0016_alter_recurrencetask_list_board_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='recurrencetask',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='todo',
|
||||||
|
name='list_board',
|
||||||
|
field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,6 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from boards.models import ListBoard, Board
|
||||||
|
|
||||||
class Tag(models.Model):
|
class Tag(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a tag that can be associated with tasks.
|
Represents a tag that can be associated with tasks.
|
||||||
@ -12,7 +14,7 @@ class Tag(models.Model):
|
|||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents a Abstract of task, such as Habit, Daily, Todo, or Reward.
|
Represents a Abstract of task, such as Habit, Recurrence, Todo.
|
||||||
|
|
||||||
:param user: The user who owns the task.
|
:param user: The user who owns the task.
|
||||||
:param title: Title of the task.
|
:param title: Title of the task.
|
||||||
@ -23,10 +25,6 @@ class Task(models.Model):
|
|||||||
:param challenge: Associated challenge (optional).
|
:param challenge: Associated challenge (optional).
|
||||||
:param fromSystem: A boolean field indicating if the task is from System.
|
:param fromSystem: A boolean field indicating if the task is from System.
|
||||||
:param creation_date: Creation date of the task.
|
:param creation_date: Creation date of the task.
|
||||||
:param last_update: Last updated date of the task.
|
|
||||||
:param: google_calendar_id: Google Calendar Event ID of the task.
|
|
||||||
:param start_event: Start event of the task.
|
|
||||||
:param end_event: End event(Due Date) of the task.
|
|
||||||
"""
|
"""
|
||||||
class Difficulty(models.IntegerChoices):
|
class Difficulty(models.IntegerChoices):
|
||||||
EASY = 1, 'Easy'
|
EASY = 1, 'Easy'
|
||||||
@ -45,22 +43,36 @@ class Task(models.Model):
|
|||||||
fromSystem = models.BooleanField(default=False)
|
fromSystem = models.BooleanField(default=False)
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
last_update = models.DateTimeField(auto_now=True)
|
last_update = models.DateTimeField(auto_now=True)
|
||||||
google_calendar_id = models.CharField(max_length=255, null=True, blank=True)
|
|
||||||
start_event = models.DateTimeField(null=True)
|
|
||||||
end_event = models.DateTimeField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Todo(Task):
|
class Todo(Task):
|
||||||
|
"""
|
||||||
|
Represent a Todo task.
|
||||||
|
|
||||||
|
:param list_board: The list board that the task belongs to.
|
||||||
|
:param is_active: A boolean field indicating whether the task is active. (Archive or not)
|
||||||
|
:param is_full_day_event: A boolean field indicating whether the task is a full day event.
|
||||||
|
:param start_event: Start date and time of the task.
|
||||||
|
:param end_event: End date and time of the task.
|
||||||
|
:param google_calendar_id: The Google Calendar ID of the task.
|
||||||
|
:param completed: A boolean field indicating whether the task is completed.
|
||||||
|
:param priority: The priority of the task (range: 1 to 4).
|
||||||
|
"""
|
||||||
class EisenhowerMatrix(models.IntegerChoices):
|
class EisenhowerMatrix(models.IntegerChoices):
|
||||||
IMPORTANT_URGENT = 1, 'Important & Urgent'
|
IMPORTANT_URGENT = 1, 'Important & Urgent'
|
||||||
IMPORTANT_NOT_URGENT = 2, 'Important & Not Urgent'
|
IMPORTANT_NOT_URGENT = 2, 'Important & Not Urgent'
|
||||||
NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent'
|
NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent'
|
||||||
NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent'
|
NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent'
|
||||||
|
|
||||||
|
list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
is_full_day_event = models.BooleanField(default=False)
|
||||||
|
start_event = models.DateTimeField(null=True)
|
||||||
|
end_event = models.DateTimeField(null=True)
|
||||||
|
google_calendar_id = models.CharField(max_length=255, null=True, blank=True)
|
||||||
completed = models.BooleanField(default=False)
|
completed = models.BooleanField(default=False)
|
||||||
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
@ -68,15 +80,95 @@ class Todo(Task):
|
|||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class RecurrenceTask(Task):
|
class RecurrenceTask(Task):
|
||||||
|
"""
|
||||||
|
Represent a Recurrence task. (Occure every day, week, month, year)
|
||||||
|
|
||||||
|
:param list_board: The list board that the task belongs to.
|
||||||
|
:param rrule: The recurrence rule of the task.
|
||||||
|
:param is_active: A boolean field indicating whether the task is active. (Archive or not)
|
||||||
|
:param is_full_day_event: A boolean field indicating whether the task is a full day event.
|
||||||
|
:param start_event: Start date and time of the task.
|
||||||
|
:param end_event: End date and time of the task.
|
||||||
|
:param completed: A boolean field indicating whether the task is completed.
|
||||||
|
:param parent_task: The parent task of the subtask.
|
||||||
|
"""
|
||||||
|
list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1)
|
||||||
|
rrule = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
is_full_day_event = models.BooleanField(default=False)
|
||||||
|
start_event = models.DateTimeField(null=True)
|
||||||
|
end_event = models.DateTimeField(null=True)
|
||||||
completed = models.BooleanField(default=False)
|
completed = models.BooleanField(default=False)
|
||||||
recurrence_rule = models.CharField()
|
parent_task = models.ForeignKey("self", on_delete=models.CASCADE, null=True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.title} ({self.recurrence_rule})"
|
return f"{self.title} ({self.recurrence_rule})"
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrencePattern(models.Model):
|
||||||
|
"""
|
||||||
|
:param recurrence_task: The recurrence task that the pattern belongs to.
|
||||||
|
:param recurring_type: The type of recurrence.
|
||||||
|
:param max_occurrences: The maximum number of occurrences.
|
||||||
|
:param day_of_week: The day of the week that event will occure.
|
||||||
|
:param week_of_month: The week of the month that event will occure.
|
||||||
|
:param day_of_month: The day of the month that event will occure.
|
||||||
|
:param month_of_year: The month of the year that event will occure.
|
||||||
|
"""
|
||||||
|
class RecurringType(models.IntegerChoices):
|
||||||
|
DAILY = 0, 'Daily'
|
||||||
|
WEEKLY = 1, 'Weekly'
|
||||||
|
MONTHLY = 2, 'Monthly'
|
||||||
|
YEARLY = 3, 'Yearly'
|
||||||
|
|
||||||
|
class DayOfWeek(models.IntegerChoices):
|
||||||
|
MONDAY = 0, 'Monday'
|
||||||
|
TUESDAY = 1, 'Tuesday'
|
||||||
|
WEDNESDAY = 2, 'Wednesday'
|
||||||
|
THURSDAY = 3, 'Thursday'
|
||||||
|
FRIDAY = 4, 'Friday'
|
||||||
|
SATURDAY = 5, 'Saturday'
|
||||||
|
SUNDAY = 6, 'Sunday'
|
||||||
|
|
||||||
|
class WeekOfMonth(models.IntegerChoices):
|
||||||
|
FIRST = 1, 'First'
|
||||||
|
SECOND = 2, 'Second'
|
||||||
|
THIRD = 3, 'Third'
|
||||||
|
FOURTH = 4, 'Fourth'
|
||||||
|
LAST = 5, 'Last'
|
||||||
|
|
||||||
|
class MonthOfYear(models.IntegerChoices):
|
||||||
|
JANUARY = 1, 'January'
|
||||||
|
FEBRUARY = 2, 'February'
|
||||||
|
MARCH = 3, 'March'
|
||||||
|
APRIL = 4, 'April'
|
||||||
|
MAY = 5, 'May'
|
||||||
|
JUNE = 6, 'June'
|
||||||
|
JULY = 7, 'July'
|
||||||
|
AUGUST = 8, 'August'
|
||||||
|
SEPTEMBER = 9, 'September'
|
||||||
|
OCTOBER = 10, 'October'
|
||||||
|
NOVEMBER = 11, 'November'
|
||||||
|
DECEMBER = 12, 'December'
|
||||||
|
|
||||||
|
recurrence_task = models.ForeignKey(RecurrenceTask, on_delete=models.CASCADE)
|
||||||
|
recurring_type = models.IntegerField(choices=RecurringType.choices)
|
||||||
|
max_occurrences = models.IntegerField(default=0)
|
||||||
|
day_of_week = models.IntegerField(choices=DayOfWeek.choices)
|
||||||
|
week_of_month = models.IntegerField(choices=WeekOfMonth.choices)
|
||||||
|
day_of_month = models.IntegerField(default=0)
|
||||||
|
month_of_year = models.IntegerField(choices=MonthOfYear.choices)
|
||||||
|
|
||||||
|
|
||||||
class Habit(Task):
|
class Habit(Task):
|
||||||
|
"""
|
||||||
|
Represent a Habit task with streaks.
|
||||||
|
|
||||||
|
:param streak: The streak of the habit.
|
||||||
|
:param current_count: The current count of the habit.
|
||||||
|
"""
|
||||||
streak = models.IntegerField(default=0)
|
streak = models.IntegerField(default=0)
|
||||||
|
current_count = models.IntegerField(default=0)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.title} ({self.streak})"
|
return f"{self.title} ({self.streak})"
|
||||||
@ -91,67 +183,4 @@ class Subtask(models.Model):
|
|||||||
"""
|
"""
|
||||||
parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE)
|
parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
completed = models.BooleanField(default=False)
|
completed = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class UserNotification(models.Model):
|
|
||||||
"""
|
|
||||||
Represents a user notification.
|
|
||||||
|
|
||||||
:param type: The type of the notification (e.g., 'NEW_CHAT_MESSAGE').
|
|
||||||
:param data: JSON data associated with the notification.
|
|
||||||
:param seen: A boolean field indicating whether the notification has been seen.
|
|
||||||
"""
|
|
||||||
NOTIFICATION_TYPES = (
|
|
||||||
('LEVEL_UP', 'Level Up'),
|
|
||||||
('DEATH', 'Death'),
|
|
||||||
)
|
|
||||||
|
|
||||||
type = models.CharField(max_length=255, choices=[type for type in NOTIFICATION_TYPES])
|
|
||||||
data = models.JSONField(default=dict)
|
|
||||||
seen = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clean_notification(notifications):
|
|
||||||
"""
|
|
||||||
Cleanup function for removing corrupt notification data:
|
|
||||||
- Removes notifications with null or missing id or type.
|
|
||||||
"""
|
|
||||||
if not notifications:
|
|
||||||
return notifications
|
|
||||||
|
|
||||||
filtered_notifications = []
|
|
||||||
|
|
||||||
for notification in notifications:
|
|
||||||
if notification.id is None or notification.type is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return filtered_notifications
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(models.Model):
|
|
||||||
"""
|
|
||||||
Represents a transaction involving currencies in the system.
|
|
||||||
|
|
||||||
:param currency: The type of currency used in the transaction
|
|
||||||
:param transactionType: The type of the transaction
|
|
||||||
:param description: Additional text.
|
|
||||||
:param amount: The transaction amount.
|
|
||||||
:param user: The user involved in the transaction.
|
|
||||||
"""
|
|
||||||
CURRENCIES = (('gold', 'Gold'),)
|
|
||||||
TRANSACTION_TYPES = (
|
|
||||||
('buy_gold', 'Buy Gold'),
|
|
||||||
('spend', 'Spend'),
|
|
||||||
('debug', 'Debug'),
|
|
||||||
('force_update_gold', 'Force Update Gold'),
|
|
||||||
)
|
|
||||||
|
|
||||||
currency = models.CharField(max_length=12, choices=CURRENCIES)
|
|
||||||
transaction_type = models.CharField(max_length=24, choices=TRANSACTION_TYPES)
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
amount = models.FloatField(default=0)
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Transaction ({self.id})"
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.dateparse import parse_datetime
|
|
||||||
from .models import Todo, RecurrenceTask
|
from .models import Todo, RecurrenceTask
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from boards.models import ListBoard
|
||||||
from tasks.models import Todo
|
from tasks.models import Todo
|
||||||
|
|
||||||
|
|
||||||
@ -22,4 +23,17 @@ def update_priority(sender, instance, **kwargs):
|
|||||||
elif time_until_due <= urgency_threshold and instance.importance < importance_threshold:
|
elif time_until_due <= urgency_threshold and instance.importance < importance_threshold:
|
||||||
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT
|
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT
|
||||||
else:
|
else:
|
||||||
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
|
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Todo)
|
||||||
|
def assign_todo_to_listboard(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
user_board = instance.user.board_set.first()
|
||||||
|
|
||||||
|
if user_board:
|
||||||
|
first_list_board = user_board.listboard_set.order_by('position').first()
|
||||||
|
|
||||||
|
if first_list_board:
|
||||||
|
instance.list_board = first_list_board
|
||||||
|
instance.save()
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-19 20:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0005_alter_userstats_endurance_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='endurance',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='intelligence',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='luck',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='perception',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userstats',
|
||||||
|
name='strength',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='last_name',
|
||||||
|
field=models.CharField(blank=True, max_length=150),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -5,16 +5,18 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
||||||
|
|
||||||
from .managers import CustomAccountManager
|
from .managers import CustomAccountManager
|
||||||
|
|
||||||
|
|
||||||
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||||
# User fields
|
"""
|
||||||
|
User model where email is the unique identifier for authentication.
|
||||||
|
"""
|
||||||
email = models.EmailField(_('email address'), unique=True)
|
email = models.EmailField(_('email address'), unique=True)
|
||||||
username = models.CharField(max_length=150, unique=True)
|
username = models.CharField(max_length=150, unique=True)
|
||||||
first_name = models.CharField(max_length=150, blank=True)
|
first_name = models.CharField(max_length=150, blank=True)
|
||||||
|
last_name = models.CharField(max_length=150, blank=True)
|
||||||
start_date = models.DateTimeField(default=timezone.now)
|
start_date = models.DateTimeField(default=timezone.now)
|
||||||
about = models.TextField(_('about'), max_length=500, blank=True)
|
about = models.TextField(_('about'), max_length=500, blank=True)
|
||||||
profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png')
|
profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png')
|
||||||
@ -35,7 +37,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||||||
# String representation of the user
|
# String representation of the user
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
def random_luck():
|
def random_luck():
|
||||||
return random.randint(1, 50)
|
return random.randint(1, 50)
|
||||||
|
|
||||||
@ -51,17 +52,6 @@ class UserStats(models.Model):
|
|||||||
health = models.IntegerField(default=100)
|
health = models.IntegerField(default=100)
|
||||||
gold = models.FloatField(default=0.0)
|
gold = models.FloatField(default=0.0)
|
||||||
experience = models.FloatField(default=0)
|
experience = models.FloatField(default=0)
|
||||||
strength = models.IntegerField(default=1,
|
|
||||||
validators=[MinValueValidator(1),
|
|
||||||
MaxValueValidator(100)])
|
|
||||||
intelligence = models.IntegerField(default=1, validators=[MinValueValidator(1),
|
|
||||||
MaxValueValidator(100)])
|
|
||||||
endurance = models.IntegerField(default=1, validators=[MinValueValidator(1),
|
|
||||||
MaxValueValidator(100)])
|
|
||||||
perception = models.IntegerField(default=1, validators=[MinValueValidator(1),
|
|
||||||
MaxValueValidator(100)])
|
|
||||||
luck = models.IntegerField(default=random_luck, validators=[MinValueValidator(1),
|
|
||||||
MaxValueValidator(50)],)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level(self):
|
def level(self):
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> -->
|
<!-- <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> -->
|
||||||
<title>Vite + React</title>
|
<title>TurTask</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -10,23 +10,27 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@fullcalendar/core": "^6.1.9",
|
"@fullcalendar/core": "^6.1.9",
|
||||||
"@fullcalendar/daygrid": "^6.1.9",
|
"@fullcalendar/daygrid": "^6.1.9",
|
||||||
"@fullcalendar/interaction": "^6.1.9",
|
"@fullcalendar/interaction": "^6.1.9",
|
||||||
"@fullcalendar/react": "^6.1.9",
|
"@fullcalendar/react": "^6.1.9",
|
||||||
"@fullcalendar/timegrid": "^6.1.9",
|
"@fullcalendar/timegrid": "^6.1.9",
|
||||||
|
"@heroicons/react": "1.0.6",
|
||||||
"@mui/icons-material": "^5.14.16",
|
"@mui/icons-material": "^5.14.16",
|
||||||
"@mui/material": "^5.14.17",
|
"@mui/material": "^5.14.17",
|
||||||
"@mui/system": "^5.14.17",
|
"@mui/system": "^5.14.17",
|
||||||
"@react-oauth/google": "^0.11.1",
|
"@react-oauth/google": "^0.11.1",
|
||||||
"@syncfusion/ej2-base": "^23.1.41",
|
"@syncfusion/ej2-base": "^23.1.41",
|
||||||
"@syncfusion/ej2-kanban": "^23.1.36",
|
"@syncfusion/ej2-kanban": "^23.1.36",
|
||||||
|
"@tremor/react": "^3.11.1",
|
||||||
"axios": "^1.6.1",
|
"axios": "^1.6.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@ -36,9 +40,12 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-bootstrap": "^2.9.1",
|
"react-bootstrap": "^2.9.1",
|
||||||
|
"react-datetime-picker": "^5.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-router-dom": "^6.18.0"
|
"react-router-dom": "^6.18.0",
|
||||||
|
"react-tsparticles": "^2.12.2",
|
||||||
|
"tsparticles": "^2.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -43,4 +43,4 @@
|
|||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,13 +5,13 @@ 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/navigations/Navbar";
|
import NavBar from "./components/navigations/Navbar";
|
||||||
import Home from "./components/Home";
|
|
||||||
import Calendar from "./components/calendar/calendar";
|
import Calendar from "./components/calendar/calendar";
|
||||||
import KanbanPage from "./components/kanbanBoard/kanbanPage";
|
import KanbanPage from "./components/kanbanBoard/kanbanPage";
|
||||||
import IconSideNav from "./components/navigations/IconSideNav";
|
import IconSideNav from "./components/navigations/IconSideNav";
|
||||||
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
|
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
|
||||||
import PrivateRoute from "./PrivateRoute";
|
import PrivateRoute from "./PrivateRoute";
|
||||||
import ProfileUpdatePage from "./components/profilePage";
|
import ProfileUpdatePage from "./components/profilePage";
|
||||||
|
import Dashboard from "./components/dashboard/dashboard";
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@ -26,7 +26,7 @@ const App = () => {
|
|||||||
<NavBar />
|
<NavBar />
|
||||||
<div className={isLoginPageOrSignUpPage ? "" : "overflow-x-auto"}>
|
<div className={isLoginPageOrSignUpPage ? "" : "overflow-x-auto"}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route exact path="/tasks" element={<PrivateRoute />}>
|
<Route exact path="/tasks" element={<PrivateRoute />}>
|
||||||
<Route exact path="/tasks" element={<KanbanPage />} />
|
<Route exact path="/tasks" element={<KanbanPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -8,4 +8,4 @@ const PrivateRoute = () => {
|
|||||||
return auth ? <Outlet /> : <Navigate to="/login" />;
|
return auth ? <Outlet /> : <Navigate to="/login" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PrivateRoute;
|
export default PrivateRoute;
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
function HomePage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Welcome to My Website</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomePage;
|
|
||||||
@ -1,15 +1,17 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useGoogleLogin } from "@react-oauth/google";
|
import { useGoogleLogin } from "@react-oauth/google";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import Particles from "react-tsparticles";
|
||||||
|
import { loadFull } from "tsparticles";
|
||||||
import refreshAccessToken from "./refreshAcesstoken";
|
import refreshAccessToken from "./refreshAcesstoken";
|
||||||
import axiosapi from "../../api/AuthenticationApi";
|
import axiosapi from "../../api/AuthenticationApi";
|
||||||
|
import { useAuth } from "../../hooks/authentication/IsAuthenticated";
|
||||||
|
import { FcGoogle } from "react-icons/fc";
|
||||||
|
|
||||||
import { useAuth } from "../../hooks/authentication/IsAuthenticated";
|
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,15 +23,14 @@ function LoginPage() {
|
|||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const handleEmailChange = event => {
|
const handleEmailChange = (event) => {
|
||||||
setEmail(event.target.value);
|
setEmail(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordChange = event => {
|
const handlePasswordChange = (event) => {
|
||||||
setPassword(event.target.value);
|
setPassword(event.target.value);
|
||||||
};
|
};
|
||||||
|
const handleSubmit = (event) => {
|
||||||
const handleSubmit = event => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Send a POST request to the authentication API
|
// Send a POST request to the authentication API
|
||||||
@ -38,15 +39,16 @@ function LoginPage() {
|
|||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
// On successful login, store tokens and set the authorization header
|
// On successful login, store tokens and set the authorization header
|
||||||
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);
|
setIsAuthenticated(true);
|
||||||
Navigate("/");
|
Navigate("/");
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.log("Login failed");
|
console.log("Login failed");
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
@ -56,7 +58,7 @@ function LoginPage() {
|
|||||||
const googleLoginImplicit = useGoogleLogin({
|
const googleLoginImplicit = useGoogleLogin({
|
||||||
flow: "auth-code",
|
flow: "auth-code",
|
||||||
redirect_uri: "postmessage",
|
redirect_uri: "postmessage",
|
||||||
onSuccess: async response => {
|
onSuccess: async (response) => {
|
||||||
try {
|
try {
|
||||||
const loginResponse = await axiosapi.googleLogin(response.code);
|
const loginResponse = await axiosapi.googleLogin(response.code);
|
||||||
if (loginResponse && loginResponse.data) {
|
if (loginResponse && loginResponse.data) {
|
||||||
@ -72,17 +74,104 @@ function LoginPage() {
|
|||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: errorResponse => console.log(errorResponse),
|
onError: (errorResponse) => console.log(errorResponse),
|
||||||
});
|
});
|
||||||
|
{
|
||||||
|
/* Particles Loader*/
|
||||||
|
}
|
||||||
|
const particlesInit = useCallback(async (engine) => {
|
||||||
|
console.log(engine);
|
||||||
|
await loadFull(engine);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const particlesLoaded = useCallback(async (container) => {
|
||||||
|
console.log(container);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-theme="night" className="min-h-screen flex">
|
<div
|
||||||
{/* Left Section (Login Box) */}
|
data-theme="night"
|
||||||
<div className="w-1/2 flex items-center justify-center">
|
className="h-screen flex items-center justify-center"
|
||||||
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4">
|
>
|
||||||
<h2 className="text-2xl font-semibold text-left">Log in to your account</h2>
|
{/* Particles Container */}
|
||||||
|
<div style={{ width: "0%", height: "100vh" }}>
|
||||||
|
<Particles
|
||||||
|
id="particles"
|
||||||
|
init={particlesInit}
|
||||||
|
loaded={particlesLoaded}
|
||||||
|
className="-z-10"
|
||||||
|
options={{
|
||||||
|
fpsLimit: 240,
|
||||||
|
interactivity: {
|
||||||
|
events: {
|
||||||
|
onClick: {
|
||||||
|
enable: true,
|
||||||
|
mode: "push",
|
||||||
|
},
|
||||||
|
onHover: {
|
||||||
|
enable: true,
|
||||||
|
mode: "repulse",
|
||||||
|
},
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
push: {
|
||||||
|
quantity: 4,
|
||||||
|
},
|
||||||
|
repulse: {
|
||||||
|
distance: 200,
|
||||||
|
duration: 0.4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
particles: {
|
||||||
|
color: {
|
||||||
|
value: "#008000",
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
color: "#00ff00",
|
||||||
|
distance: 150,
|
||||||
|
enable: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
direction: "none",
|
||||||
|
enable: true,
|
||||||
|
outModes: {
|
||||||
|
default: "bounce",
|
||||||
|
},
|
||||||
|
random: false,
|
||||||
|
speed: 4,
|
||||||
|
straight: false,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
density: {
|
||||||
|
enable: true,
|
||||||
|
area: 800,
|
||||||
|
},
|
||||||
|
value: 50,
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
value: 0.5,
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
type: "circle",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
value: { min: 4, max: 5 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detectRetina: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Login Box */}
|
||||||
|
<div className="w-1/4 flex items-center justify-center">
|
||||||
|
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4 z-10">
|
||||||
|
<h2 className="text-3xl font-bold text-center">Login</h2>
|
||||||
{/* Email Input */}
|
{/* Email Input */}
|
||||||
<div className="form-control">
|
<div className="form-control ">
|
||||||
<label className="label" htmlFor="email">
|
<label className="label" htmlFor="email">
|
||||||
<p className="text-bold">
|
<p className="text-bold">
|
||||||
Email<span className="text-red-500 text-bold">*</span>
|
Email<span className="text-red-500 text-bold">*</span>
|
||||||
@ -112,13 +201,16 @@ function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Login Button */}
|
{/* Login Button */}
|
||||||
<button className="btn btn-primary w-full" onClick={handleSubmit}>
|
<button className="btn btn-success w-full " onClick={handleSubmit}>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
<div className="divider">OR</div>
|
<div className="divider">OR</div>
|
||||||
{/* Login with Google Button */}
|
{/* Login with Google Button */}
|
||||||
<button className="btn btn-outline btn-secondary w-full" onClick={() => googleLoginImplicit()}>
|
<button
|
||||||
Login with Google
|
className="btn btn-outline btn-secondary w-full "
|
||||||
|
onClick={() => googleLoginImplicit()}
|
||||||
|
>
|
||||||
|
<FcGoogle className="rounded-full bg-white"/>Login with Google
|
||||||
</button>
|
</button>
|
||||||
{/* Forgot Password Link */}
|
{/* Forgot Password Link */}
|
||||||
<div className="justify-left">
|
<div className="justify-left">
|
||||||
@ -128,20 +220,6 @@ function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Section (Blurred Image Background) */}
|
|
||||||
<div className="w-1/2 relative">
|
|
||||||
<div
|
|
||||||
className="w-full h-full bg-cover bg-center"
|
|
||||||
style={{
|
|
||||||
backgroundImage: 'url("https://th.bing.com/th/id/OIG.9byG0pWUCcbGL7Kly9tA?pid=ImgGn&w=1024&h=1024&rs=1")',
|
|
||||||
filter: "blur(2px) brightness(.5)",
|
|
||||||
}}></div>
|
|
||||||
|
|
||||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-2xl font-semibold">
|
|
||||||
Text Overlay
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,29 @@
|
|||||||
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 { useCallback } from "react";
|
||||||
|
import Particles from "react-tsparticles";
|
||||||
|
import { loadFull } from "tsparticles";
|
||||||
|
import { FcGoogle } from "react-icons/fc";
|
||||||
|
import { useGoogleLogin } from "@react-oauth/google";
|
||||||
|
|
||||||
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}>
|
<div className="text-center text-sm text-gray-500" {...props}>
|
||||||
{"Copyright © "}
|
{"Copyright © "}
|
||||||
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
|
<a
|
||||||
|
href="https://github.com/TurTaskProject/TurTaskWeb"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
TurTask
|
TurTask
|
||||||
</Link>{" "}
|
</a>{" "}
|
||||||
{new Date().getFullYear()}
|
{new Date().getFullYear()}
|
||||||
{"."}
|
{"."}
|
||||||
</Typography>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTheme = createTheme();
|
|
||||||
|
|
||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
@ -42,7 +35,7 @@ export default function SignUp() {
|
|||||||
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);
|
||||||
@ -58,86 +51,194 @@ export default function SignUp() {
|
|||||||
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 });
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
/* Particles Loader*/
|
||||||
|
}
|
||||||
|
const particlesInit = useCallback(async (engine) => {
|
||||||
|
console.log(engine);
|
||||||
|
await loadFull(engine);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const particlesLoaded = useCallback(async (container) => {
|
||||||
|
console.log(container);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const googleLoginImplicit = useGoogleLogin({
|
||||||
|
flow: "auth-code",
|
||||||
|
redirect_uri: "postmessage",
|
||||||
|
onSuccess: async (response) => {
|
||||||
|
try {
|
||||||
|
const loginResponse = await axiosapi.googleLogin(response.code);
|
||||||
|
if (loginResponse && loginResponse.data) {
|
||||||
|
const { access_token, refresh_token } = loginResponse.data;
|
||||||
|
|
||||||
|
localStorage.setItem("access_token", access_token);
|
||||||
|
localStorage.setItem("refresh_token", refresh_token);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
Navigate("/");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error with the POST request:", error);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (errorResponse) => console.log(errorResponse),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={defaultTheme}>
|
<div
|
||||||
<Container component="main" maxWidth="xs">
|
data-theme="night"
|
||||||
<CssBaseline />
|
className="h-screen flex items-center justify-center"
|
||||||
<Box
|
>
|
||||||
sx={{
|
{/* Particles Container */}
|
||||||
marginTop: 8,
|
<div style={{ width: "0%", height: "100vh" }}>
|
||||||
display: "flex",
|
<Particles
|
||||||
flexDirection: "column",
|
id="particles"
|
||||||
alignItems: "center",
|
init={particlesInit}
|
||||||
}}>
|
loaded={particlesLoaded}
|
||||||
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
|
className="-z-10"
|
||||||
<LockOutlinedIcon />
|
options={{
|
||||||
</Avatar>
|
fpsLimit: 240,
|
||||||
<Typography component="h1" variant="h5">
|
interactivity: {
|
||||||
Sign up
|
events: {
|
||||||
</Typography>
|
onClick: {
|
||||||
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
|
enable: true,
|
||||||
<Grid container spacing={2}>
|
mode: "push",
|
||||||
<Grid item xs={12}>
|
},
|
||||||
<TextField
|
onHover: {
|
||||||
required
|
enable: true,
|
||||||
fullWidth
|
mode: "repulse",
|
||||||
id="email"
|
},
|
||||||
label="Email Address"
|
resize: true,
|
||||||
name="email"
|
},
|
||||||
autoComplete="email"
|
modes: {
|
||||||
onChange={handleChange}
|
push: {
|
||||||
/>
|
quantity: 4,
|
||||||
</Grid>
|
},
|
||||||
<Grid item xs={12}>
|
repulse: {
|
||||||
<TextField
|
distance: 200,
|
||||||
autoComplete="username"
|
duration: 0.4,
|
||||||
name="Username"
|
},
|
||||||
required
|
},
|
||||||
fullWidth
|
},
|
||||||
id="Username"
|
particles: {
|
||||||
label="Username"
|
color: {
|
||||||
autoFocus
|
value: "#023020",
|
||||||
onChange={handleChange}
|
},
|
||||||
/>
|
links: {
|
||||||
</Grid>
|
color: "#228B22",
|
||||||
<Grid item xs={12}>
|
distance: 150,
|
||||||
<TextField
|
enable: true,
|
||||||
required
|
opacity: 1,
|
||||||
fullWidth
|
width: 1,
|
||||||
name="password"
|
},
|
||||||
label="Password"
|
move: {
|
||||||
type="password"
|
direction: "none",
|
||||||
id="password"
|
enable: true,
|
||||||
autoComplete="new-password"
|
outModes: {
|
||||||
onChange={handleChange}
|
default: "bounce",
|
||||||
/>
|
},
|
||||||
</Grid>
|
random: false,
|
||||||
<Grid item xs={12}>
|
speed: 4,
|
||||||
<FormControlLabel
|
straight: false,
|
||||||
control={<Checkbox value="allowExtraEmails" color="primary" />}
|
},
|
||||||
label="I want to receive inspiration, marketing promotions and updates via email."
|
number: {
|
||||||
/>
|
density: {
|
||||||
</Grid>
|
enable: true,
|
||||||
</Grid>
|
area: 800,
|
||||||
<Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
|
},
|
||||||
Sign Up
|
value: 50,
|
||||||
</Button>
|
},
|
||||||
<Grid container justifyContent="flex-end">
|
opacity: {
|
||||||
<Grid item>
|
value: 0.6,
|
||||||
<Link href="#" variant="body2">
|
},
|
||||||
Already have an account? Sign in
|
shape: {
|
||||||
</Link>
|
type: "circle",
|
||||||
</Grid>
|
},
|
||||||
</Grid>
|
size: {
|
||||||
</Box>
|
value: { min: 6, max: 8 },
|
||||||
</Box>
|
},
|
||||||
<Copyright sx={{ mt: 5 }} />
|
},
|
||||||
</Container>
|
detectRetina: true,
|
||||||
</ThemeProvider>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/4 h-1 flex items-center justify-center">
|
||||||
|
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4 z-10">
|
||||||
|
{/* Register Form */}
|
||||||
|
<h2 className="text-3xl font-bold text-center">Signup</h2>
|
||||||
|
{/* Email Input */}
|
||||||
|
<div className="form-control ">
|
||||||
|
<label className="label" htmlFor="email">
|
||||||
|
<p className="text-bold">
|
||||||
|
Email<span className="text-red-500 text-bold">*</span>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Username Input */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label" htmlFor="Username">
|
||||||
|
<p className="text-bold">
|
||||||
|
Username<span className="text-red-500 text-bold">*</span>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
id="Username"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Password Input */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label" htmlFor="password">
|
||||||
|
<p className="text-bold">
|
||||||
|
Password<span className="text-red-500 text-bold">*</span>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
{/* Login Button */}
|
||||||
|
<button className="btn btn-success w-full " onClick={handleSubmit}>
|
||||||
|
Signup
|
||||||
|
</button>
|
||||||
|
<div className="divider">OR</div>
|
||||||
|
{/* Login with Google Button */}
|
||||||
|
<button
|
||||||
|
className="btn btn-outline btn-secondary w-full "
|
||||||
|
onClick={() => googleLoginImplicit()}
|
||||||
|
>
|
||||||
|
<FcGoogle className="rounded-full bg-white"/>Login with Google
|
||||||
|
</button>
|
||||||
|
{/* Already have an account? */}
|
||||||
|
<div className="text-blue-500 flex justify-center text-sm">
|
||||||
|
<a href="login">
|
||||||
|
Already have an account?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Copyright />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
93
frontend/src/components/dashboard/Barchart.jsx
Normal file
93
frontend/src/components/dashboard/Barchart.jsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { BarChart, Card, Title } from "@tremor/react";
|
||||||
|
import React from "react";
|
||||||
|
import axiosInstance from "../../api/configs/AxiosConfig";
|
||||||
|
|
||||||
|
const apiGetBarChartData = () => {
|
||||||
|
return axiosInstance.get("dashboard/stats/");
|
||||||
|
}
|
||||||
|
console.log(apiGetBarChartData);
|
||||||
|
|
||||||
|
const chartdata3 = [
|
||||||
|
{
|
||||||
|
date: "Jan 23",
|
||||||
|
"2022": 45,
|
||||||
|
"2023": 78,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Feb 23",
|
||||||
|
"2022": 52,
|
||||||
|
"2023": 71,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Mar 23",
|
||||||
|
"2022": 48,
|
||||||
|
"2023": 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Apr 23",
|
||||||
|
"2022": 61,
|
||||||
|
"2023": 65,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "May 23",
|
||||||
|
"2022": 55,
|
||||||
|
"2023": 58,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jun 23",
|
||||||
|
"2022": 67,
|
||||||
|
"2023": 62,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jul 23",
|
||||||
|
"2022": 60,
|
||||||
|
"2023": 54,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Aug 23",
|
||||||
|
"2022": 72,
|
||||||
|
"2023": 49,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Sep 23",
|
||||||
|
"2022": 65,
|
||||||
|
"2023": 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Oct 23",
|
||||||
|
"2022": 68,
|
||||||
|
"2023": null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Nov 23",
|
||||||
|
"2022": 74,
|
||||||
|
"2023": null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Dec 23",
|
||||||
|
"2022": 71,
|
||||||
|
"2023": null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BarChartGraph = () => {
|
||||||
|
const [value, setValue] = React.useState(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<Title>Closed Pull Requests</Title>
|
||||||
|
<BarChart
|
||||||
|
className="mt-6"
|
||||||
|
data={chartdata3}
|
||||||
|
index="date"
|
||||||
|
categories={["2022", "2023"]}
|
||||||
|
colors={["neutral", "indigo"]}
|
||||||
|
yAxisWidth={30}
|
||||||
|
onValueChange={(v) => setValue(v)}
|
||||||
|
showAnimation
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<pre>{JSON.stringify(value)}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
frontend/src/components/dashboard/KpiCard.jsx
Normal file
21
frontend/src/components/dashboard/KpiCard.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react";
|
||||||
|
|
||||||
|
export default function KpiCard() {
|
||||||
|
return (
|
||||||
|
<Card className="max-w-lg mx-auto">
|
||||||
|
<Flex alignItems="start">
|
||||||
|
<div>
|
||||||
|
<Text>Sales</Text>
|
||||||
|
<Metric>$ 12,699</Metric>
|
||||||
|
</div>
|
||||||
|
<BadgeDelta deltaType="moderateIncrease">13.2%</BadgeDelta>
|
||||||
|
</Flex>
|
||||||
|
<Flex className="mt-4">
|
||||||
|
<Text className="truncate">68% ($ 149,940)</Text>
|
||||||
|
<Text>$ 220,500</Text>
|
||||||
|
</Flex>
|
||||||
|
<ProgressBar value={15.9} className="mt-2" />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
frontend/src/components/dashboard/dashboard.jsx
Normal file
64
frontend/src/components/dashboard/dashboard.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, DonutChart } from "@tremor/react";
|
||||||
|
import KpiCard from "./kpiCard";
|
||||||
|
import { BarChartGraph } from "./Barchart";
|
||||||
|
|
||||||
|
const cities = [
|
||||||
|
{
|
||||||
|
name: "New York",
|
||||||
|
sales: 9800,
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
{
|
||||||
|
name: "Zurich",
|
||||||
|
sales: 1398,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const valueFormatter = number => `$ ${new Intl.NumberFormat("us").format(number).toString()}`;
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col p-12">
|
||||||
|
<div>
|
||||||
|
<Title>Dashboard</Title>
|
||||||
|
<Text>All of your progress will be shown right here.</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TabGroup className="mt-6">
|
||||||
|
<TabList>
|
||||||
|
<Tab>Overview</Tab>
|
||||||
|
<Tab>Detail</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<Grid numItemsMd={2} numItemsLg={3} className="gap-6 mt-6">
|
||||||
|
<Card>
|
||||||
|
<KpiCard />
|
||||||
|
<br />
|
||||||
|
<KpiCard />
|
||||||
|
</Card>
|
||||||
|
{/* Placeholder to set height */}
|
||||||
|
<div className="h-31">
|
||||||
|
<Card className="mx-auto h-full">
|
||||||
|
<Title>Sales</Title>
|
||||||
|
<DonutChart
|
||||||
|
className="mt-6"
|
||||||
|
data={cities}
|
||||||
|
category="sales"
|
||||||
|
index="name"
|
||||||
|
colors={["rose", "yellow", "orange", "indigo", "blue", "emerald"]}
|
||||||
|
onValueChange={v => setValue(v)}
|
||||||
|
showAnimation
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<BarChartGraph />
|
||||||
|
</Grid>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -136,6 +136,7 @@ function KanbanBoard() {
|
|||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
|
{/* create new column */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createNewColumn();
|
createNewColumn();
|
||||||
@ -203,7 +204,7 @@ function KanbanBoard() {
|
|||||||
if (task.id !== id) return task;
|
if (task.id !== id) return task;
|
||||||
return { ...task, content };
|
return { ...task, content };
|
||||||
});
|
});
|
||||||
|
if (content === "") return deleteTask(id);
|
||||||
setTasks(newTasks);
|
setTasks(newTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,152 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FaTasks, FaRegListAlt } from "react-icons/fa";
|
||||||
|
import { FaPlus } from "react-icons/fa6";
|
||||||
|
import { TbChecklist } from "react-icons/tb";
|
||||||
|
|
||||||
function TaskDetailModal() {
|
function TaskDetailModal() {
|
||||||
|
const [difficulty, setDifficulty] = useState(50);
|
||||||
|
const [isChallengeChecked, setChallengeChecked] = useState(true);
|
||||||
|
const [isImportantChecked, setImportantChecked] = useState(true);
|
||||||
|
|
||||||
|
const handleChallengeChange = () => {
|
||||||
|
setChallengeChecked(!isChallengeChecked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportantChange = () => {
|
||||||
|
setImportantChecked(!isImportantChecked);
|
||||||
|
};
|
||||||
|
const handleDifficultyChange = event => {
|
||||||
|
setDifficulty(parseInt(event.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id="task_detail_modal" className="modal">
|
<dialog id="task_detail_modal" className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box w-4/5 max-w-3xl">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="flex flex-col py-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h3 className="font-bold text-lg">
|
||||||
|
<span className="flex gap-2">{<FaTasks className="my-2" />}Title</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs">Todo List</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="flex flex-col py-2 pb-4">
|
||||||
|
<div className="flex flex-row space-x-5">
|
||||||
|
<div className="dropdown">
|
||||||
|
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
|
||||||
|
+ Add Tags
|
||||||
|
</label>
|
||||||
|
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<a>
|
||||||
|
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
||||||
|
Item 2
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>
|
||||||
|
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
||||||
|
Item 2
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>
|
||||||
|
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
||||||
|
Item 2
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-nowrap overflow-x-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="font-bold">
|
||||||
|
<span className="flex gap-2">
|
||||||
|
<FaRegListAlt className="my-1" />
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<textarea className="textarea w-full" disabled></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Difficulty, Challenge and Importance */}
|
||||||
|
<div className="flex flex-row space-x-3 my-4">
|
||||||
|
<div className="flex-1 card shadow border-2 p-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="difficultySelector"
|
||||||
|
min={0}
|
||||||
|
max="100"
|
||||||
|
value={difficulty}
|
||||||
|
className="range"
|
||||||
|
step="25"
|
||||||
|
onChange={handleDifficultyChange}
|
||||||
|
/>
|
||||||
|
<div className="w-full flex justify-between text-xs px-2 space-x-2">
|
||||||
|
<span>Easy</span>
|
||||||
|
<span>Normal</span>
|
||||||
|
<span>Hard</span>
|
||||||
|
<span>Very Hard</span>
|
||||||
|
<span>Devil</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Challenge Checkbox */}
|
||||||
|
<div className="card shadow border-2 p-2">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer space-x-2">
|
||||||
|
<span className="label-text">Challenge</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChallengeChecked}
|
||||||
|
className="checkbox"
|
||||||
|
onChange={handleChallengeChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Important Checkbox */}
|
||||||
|
<div className="card shadow border-2 p-2">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer space-x-2">
|
||||||
|
<span className="label-text">Important</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isImportantChecked}
|
||||||
|
className="checkbox"
|
||||||
|
onChange={handleImportantChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtask */}
|
||||||
|
<div className="flex flex-col pt-2">
|
||||||
|
<h2 className="font-bold">
|
||||||
|
<span className="flex gap-1">
|
||||||
|
<TbChecklist className="my-1" />
|
||||||
|
Subtasks
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="flex space-x-3 pt-2">
|
||||||
|
<input type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" />
|
||||||
|
<button className="btn">
|
||||||
|
<FaPlus />
|
||||||
|
Add Subtask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
|
||||||
</form>
|
</form>
|
||||||
<h3 className="font-bold text-lg">Hello!</h3>
|
|
||||||
<p className="py-4">Press ESC key or click on ✕ button to close</p>
|
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{selected && (
|
{selected && (
|
||||||
<motion.span
|
<motion.span
|
||||||
className="absolute inset-0 rounded-md bg-indigo-600 z-0"
|
className="absolute inset-0 rounded-md bg-emerald-600 z-0"
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
exit={{ scale: 0 }}></motion.span>
|
exit={{ scale: 0 }}></motion.span>
|
||||||
|
|||||||
@ -39,7 +39,7 @@ function NavBar() {
|
|||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
|
className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
|
||||||
<li>
|
<li>
|
||||||
<a href={settings.Profile} className="justify-between">
|
<a href={settings.Profile} className="justify-between">
|
||||||
Profile
|
Profile
|
||||||
|
|||||||
39
frontend/src/components/signup/Signup.jsx
Normal file
39
frontend/src/components/signup/Signup.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faGoogle, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
|
||||||
|
function Signup() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="flex flex-col items-center bg-white p-10 rounded-lg shadow-md">
|
||||||
|
<h1 className="text-4xl font-semibold mb-6">Create your account</h1>
|
||||||
|
<p className="text-gray-700 mb-6 text-lg">
|
||||||
|
Start spending more time on your own table.
|
||||||
|
</p>
|
||||||
|
<div className='font-bold'>
|
||||||
|
<div className="mb-4">
|
||||||
|
<button className="flex items-center justify-center bg-blue-500 text-white px-14 py-3 rounded-lg">
|
||||||
|
<span className="mr-3"><FontAwesomeIcon icon={faGoogle} /></span>
|
||||||
|
Sign Up with Google
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<button className="flex items-center justify-center bg-gray-800 text-white px-14 py-3 rounded-lg">
|
||||||
|
<span className="mr-3"><FontAwesomeIcon icon={faGithub} /></span>
|
||||||
|
Sign Up with Github
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button className="bg-green-500 text-white px-14 py-3 rounded-lg">
|
||||||
|
Sign Up with your email.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Signup;
|
||||||
@ -1,21 +1,103 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ["./src/**/*.{js,jsx}"],
|
content: [
|
||||||
|
"./src/**/*.{js,jsx}",
|
||||||
|
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
'sans': ['"Proxima Nova"', ...defaultTheme.fontFamily.sans],
|
sans: ['"Proxima Nova"', ...defaultTheme.fontFamily.sans],
|
||||||
|
},
|
||||||
|
colors:{
|
||||||
|
tremor: {
|
||||||
|
brand: {
|
||||||
|
faint: "#eff6ff", // blue-50
|
||||||
|
muted: "#bfdbfe", // blue-200
|
||||||
|
subtle: "#60a5fa", // blue-400
|
||||||
|
DEFAULT: "#3b82f6", // blue-500
|
||||||
|
emphasis: "#1d4ed8", // blue-700
|
||||||
|
inverted: "#ffffff", // white
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
muted: "#f9fafb", // gray-50
|
||||||
|
subtle: "#f3f4f6", // gray-100
|
||||||
|
DEFAULT: "#ffffff", // white
|
||||||
|
emphasis: "#374151", // gray-700
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
DEFAULT: "#e5e7eb", // gray-200
|
||||||
|
},
|
||||||
|
ring: {
|
||||||
|
DEFAULT: "#e5e7eb", // gray-200
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
subtle: "#9ca3af", // gray-400
|
||||||
|
DEFAULT: "#6b7280", // gray-500
|
||||||
|
emphasis: "#374151", // gray-700
|
||||||
|
strong: "#111827", // gray-900
|
||||||
|
inverted: "#ffffff", // white
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow:{
|
||||||
|
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||||
|
"tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||||
|
"tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
"tremor-small": "0.375rem",
|
||||||
|
"tremor-default": "0.5rem",
|
||||||
|
"tremor-full": "9999px",
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
"tremor-label": ["0.75rem"],
|
||||||
|
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
|
||||||
|
"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
|
||||||
|
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("daisyui"),
|
safelist: [
|
||||||
require("@tailwindcss/typography"),
|
{
|
||||||
require("daisyui")
|
pattern:
|
||||||
],
|
/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
variants: ["hover", "ui-selected"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
require("daisyui"),
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
require("daisyui"),
|
||||||
|
require("@headlessui/tailwindcss"),
|
||||||
|
],
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: ["light", "night"],
|
themes: ["light", "night"],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user