Merge branch 'feature/dashboard' of https://github.com/TurTaskProject/TurTaskWeb into feature/dashboard

This commit is contained in:
THIS ONE IS A LITTLE BIT TRICKY KRUB 2023-11-25 13:10:12 +07:00
commit eec15cf297
65 changed files with 1566 additions and 1683 deletions

View File

@ -2,13 +2,12 @@ name: Django CI
on: on:
push: push:
branches: [ "main" ] branches: ["main"]
pull_request: pull_request:
branches: [ "main" ] branches: ["main"]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -24,20 +23,21 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.11 - name: Set up Python 3.11
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.11 python-version: 3.11
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip cd backend
pip install -r requirements.txt python -m pip install --upgrade pip
- name: Run migrations pip install -r requirements.txt
run: | - name: Run migrations
cd backend run: |
python manage.py migrate cd backend
- name: Run tests python manage.py migrate
run: | - name: Run tests
cd backend run: |
python manage.py test cd backend
python manage.py test

1
.gitignore vendored
View File

@ -57,7 +57,6 @@ cover/
# Django stuff: # Django stuff:
*.log *.log
local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal

View File

@ -1,12 +1,11 @@
from django.urls import path from django.urls import path
from rest_framework_simplejwt import views as jwt_views from rest_framework_simplejwt import views as jwt_views
from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo from authentications.views import ObtainTokenPairWithCustomView, GoogleRetrieveUserInfo, CheckAccessTokenAndRefreshToken
urlpatterns = [ urlpatterns = [
path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'), path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'),
path('hello/', GreetingView.as_view(), name='hello_world'),
path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"),
path('auth/google/', GoogleRetrieveUserInfo.as_view()), path('auth/google/', GoogleRetrieveUserInfo.as_view()),
path('auth/status/', CheckAccessTokenAndRefreshToken.as_view(), name='check_token_status')
] ]

View File

@ -1,6 +1,3 @@
from django.shortcuts import render
# Create your views here.
"""This module defines API views for authentication, user creation, and a simple hello message.""" """This module defines API views for authentication, user creation, and a simple hello message."""
import json import json
@ -10,14 +7,11 @@ from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from dj_rest_auth.registration.views import SocialLoginView
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
@ -27,6 +21,31 @@ from users.managers import CustomAccountManager
from users.models import CustomUser from users.models import CustomUser
class CheckAccessTokenAndRefreshToken(APIView):
permission_classes = (AllowAny,)
JWT_authenticator = JWTAuthentication()
def post(self, request, *args, **kwargs):
access_token = request.data.get('access_token')
refresh_token = request.data.get('refresh_token')
# Check if the access token is valid
if access_token:
response = self.JWT_authenticator.authenticate(request)
if response is not None:
return Response({'status': 'true'}, status=status.HTTP_200_OK)
# Check if the refresh token is valid
if refresh_token:
try:
refresh = RefreshToken(refresh_token)
access_token = str(refresh.access_token)
return Response({'access_token': access_token}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'status': 'false'}, status=status.HTTP_401_UNAUTHORIZED)
return Response({'status': 'false'}, status=status.HTTP_400_BAD_REQUEST)
class ObtainTokenPairWithCustomView(APIView): class ObtainTokenPairWithCustomView(APIView):
""" """
Custom Token Obtain Pair View. Custom Token Obtain Pair View.
@ -45,39 +64,6 @@ class ObtainTokenPairWithCustomView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class GreetingView(APIView):
"""
Hello World View.
Returns a greeting and user information for authenticated users.
"""
permission_classes = (IsAuthenticated,)
def get(self, request):
"""
Retrieve a greeting message and user information.
"""
user = request.user
user_info = {
"username": user.username,
}
response_data = {
"message": "Hello, world!",
"user_info": user_info,
}
return Response(response_data, status=status.HTTP_200_OK)
class GoogleLogin(SocialLoginView):
"""
Google Login View.
Handles Google OAuth2 authentication.
"""
# permission_classes = (AllowAny,)
adapter_class = GoogleOAuth2Adapter
# client_class = OAuth2Client
# callback_url = 'http://localhost:8000/accounts/google/login/callback/'
class GoogleRetrieveUserInfo(APIView): class GoogleRetrieveUserInfo(APIView):
""" """
Retrieve user information from Google and create a user if not exists. Retrieve user information from Google and create a user if not exists.
@ -165,4 +151,4 @@ class GoogleRetrieveUserInfo(APIView):
response = requests.get(api_url, headers=headers) response = requests.get(api_url, headers=headers)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
raise Exception('Google API Error', response) raise Exception('Google API Error', response)

View File

@ -0,0 +1,269 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'dashboard',
'boards',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"http://localhost:8000",
"http://127.0.0.1:8000",
"http://localhost:5173",
]
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('DB_NAME', default='github_actions'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default='postgres'),
'HOST': config('DB_HOST', default='127.0.0.1'),
'PORT': config('DB_PORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

View File

@ -0,0 +1,267 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'dashboard',
'boards',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"https://turtask.vercel.app",
]
CSRF_TRUSTED_ORIGINS = ["https://turtask.vercel.app"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('PGDATABASE', default='github_actions'),
'USER': config('PGUSER', default='postgres'),
'PASSWORD': config('PGPASSWORD', default='postgres'),
'HOST': config('PGHOST', default='127.0.0.1'),
'PORT': config('PGPORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = "accesstokencache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

View File

@ -1,269 +1,6 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'. if os.environ.get("DJANGO_ENV") == "PRODUCTION":
BASE_DIR = Path(__file__).resolve().parent.parent from .production_settings import *
else:
from .local_settings import *
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'dashboard',
'boards',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"http://localhost:8000",
"http://127.0.0.1:8000",
"http://localhost:5173",
]
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('DB_NAME', default='github_actions'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default='postgres'),
'HOST': config('DB_HOST', default='127.0.0.1'),
'PORT': config('DB_PORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_wsgi_application() application = get_wsgi_application()

11
backend/railway.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "python manage.py migrate && python manage.py createcachetable accesstokencache && gunicorn --timeout 500 core.wsgi",
"restartPolicyType": "NEVER",
"restartPolicyMaxRetries": 10
}
}

View File

@ -15,4 +15,6 @@ 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 python-dateutil>=2.8
gunicorn==21.2.0
packaging==23.1

View File

@ -25,7 +25,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='userstats', model_name='userstats',
name='luck', name='luck',
field=models.IntegerField(default=users.models.random_luck, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]), field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]),
), ),
migrations.AlterField( migrations.AlterField(
model_name='userstats', model_name='userstats',

View File

@ -31,14 +31,12 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
# Fields for authentication # Fields for authentication
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name'] REQUIRED_FIELDS = []
def __str__(self): def __str__(self):
# String representation of the user # String representation of the user
return self.username return self.username
def random_luck():
return random.randint(1, 50)
class UserStats(models.Model): class UserStats(models.Model):
""" """

View File

@ -7,6 +7,8 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework_simplejwt.tokens import RefreshToken
from users.serializers import CustomUserSerializer, UpdateProfileSerializer from users.serializers import CustomUserSerializer, UpdateProfileSerializer
from users.models import CustomUser from users.models import CustomUser
@ -25,7 +27,9 @@ class CustomUserCreate(APIView):
if serializer.is_valid(): if serializer.is_valid():
user = serializer.save() user = serializer.save()
if user: if user:
return Response(serializer.data, status=status.HTTP_201_CREATED) refresh = RefreshToken.for_user(user)
return Response(data={'access_token': str(refresh.access_token), 'refresh_token': str(refresh),},
status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,20 +1,18 @@
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, env: { browser: true, es2020: true, node: true },
extends: [ extends: [
'eslint:recommended', "eslint:recommended",
'plugin:react/recommended', "plugin:react/recommended",
'plugin:react/jsx-runtime', "plugin:react/jsx-runtime",
'plugin:react-hooks/recommended', "plugin:react-hooks/recommended",
], ],
ignorePatterns: ['dist', '.eslintrc.cjs'], ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: '18.2' } }, settings: { react: { version: "18.2" } },
plugins: ['react-refresh'], plugins: ["react-refresh"],
rules: { rules: {
'react-refresh/only-export-components': [ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
'warn', "react/prop-types": 0,
{ allowConstantExport: true },
],
}, },
} };

8
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
}
}

View File

@ -38,6 +38,7 @@
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"gapi-script": "^1.2.0", "gapi-script": "^1.2.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"prop-types": "^15.8.1",
"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",

View File

@ -89,6 +89,9 @@ dependencies:
jwt-decode: jwt-decode:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
prop-types:
specifier: ^15.8.1
version: 15.8.1
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0

View File

@ -1,36 +1,91 @@
import "./App.css"; import "./App.css";
import { Route, Routes, useLocation } from "react-router-dom"; import axios from "axios";
import { useEffect } from "react";
import TestAuth from "./components/testAuth"; import { Route, Routes, Navigate } from "react-router-dom";
import LoginPage from "./components/authentication/LoginPage"; import { LoginPage } from "./components/authentication/LoginPage";
import SignUpPage from "./components/authentication/SignUpPage"; import { SignUp } from "./components/authentication/SignUpPage";
import NavBar from "./components/navigations/Navbar"; import { NavBar } from "./components/navigations/Navbar";
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 { SideNav } 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/profile/profilePage";
import Dashboard from "./components/dashboard/dashboard"; import { Dashboard } from "./components/dashboard/dashboard";
import { LandingPage } from "./components/landingPage/LandingPage";
import { PublicRoute } from "./PublicRoute";
import { useAuth } from "./hooks/AuthHooks";
const baseURL = import.meta.env.VITE_BASE_URL;
const App = () => { const App = () => {
const location = useLocation(); const { isAuthenticated, setIsAuthenticated } = useAuth();
const prevention = ["/login", "/signup"];
const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_));
useEffect(() => {
const checkLoginStatus = async () => {
const data = {
access_token: localStorage.getItem("access_token"),
refresh_token: localStorage.getItem("refresh_token"),
};
await axios
.post(`${baseURL}auth/status/`, data, {
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
})
.then((response) => {
if (response.status === 200) {
if (response.data.access_token) {
localStorage.setItem("access_token", response.data.access_token);
setIsAuthenticated(true);
} else {
setIsAuthenticated(true);
}
} else {
setIsAuthenticated(false);
}
})
.catch((error) => {});
};
checkLoginStatus();
}, [setIsAuthenticated]);
return <div>{isAuthenticated ? <AuthenticatedComponents /> : <NonAuthenticatedComponents />}</div>;
};
const NonAuthenticatedComponents = () => {
return ( return (
<div className={isLoginPageOrSignUpPage ? "" : "display: flex"}> <div>
{!isLoginPageOrSignUpPage && <IconSideNav />} <Routes>
<div className={isLoginPageOrSignUpPage ? "" : "flex-1 ml-[76px] overflow-hidden"}> <Route exact path="/l" element={<PublicRoute />}>
<Route exact path="/l" element={<LandingPage />} />
</Route>
<Route exact path="/login" element={<PublicRoute />}>
<Route exact path="/login" element={<LoginPage />} />
</Route>
<Route exact path="/signup" element={<PublicRoute />}>
<Route exact path="/signup" element={<SignUp />} />
</Route>
<Route path="*" element={<Navigate to="/l" />} />
</Routes>
</div>
);
};
const AuthenticatedComponents = () => {
return (
<div className="display: flex">
<SideNav />
<div className="flex-1 ml-[76px] overflow-hidden">
<NavBar /> <NavBar />
<div className={isLoginPageOrSignUpPage ? "" : "overflow-x-auto"}> <div className="overflow-x-auto">
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <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>
<Route path="/testAuth" element={<TestAuth />} />
<Route exact path="/profile" element={<PrivateRoute />}> <Route exact path="/profile" element={<PrivateRoute />}>
<Route exact path="/profile" element={<ProfileUpdatePage />} /> <Route exact path="/profile" element={<ProfileUpdatePage />} />
</Route> </Route>
@ -40,8 +95,7 @@ const App = () => {
<Route exact path="/priority" element={<PrivateRoute />}> <Route exact path="/priority" element={<PrivateRoute />}>
<Route exact path="/priority" element={<Eisenhower />} /> <Route exact path="/priority" element={<Eisenhower />} />
</Route> </Route>
<Route path="/login" element={<LoginPage />} /> <Route path="*" element={<Navigate to="/" />} />
<Route path="/signup" element={<SignUpPage />} />
</Routes> </Routes>
</div> </div>
</div> </div>

View File

@ -1,11 +1,7 @@
import React from "react";
import { Navigate, Outlet } from "react-router-dom"; import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "./hooks/authentication/IsAuthenticated"; import { useAuth } from "src/hooks/AuthHooks";
const PrivateRoute = () => { export const PrivateRoute = () => {
const { isAuthenticated, setIsAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const auth = isAuthenticated; return isAuthenticated ? <Outlet /> : <Navigate to="/" />;
return auth ? <Outlet /> : <Navigate to="/login" />;
}; };
export default PrivateRoute;

View File

@ -0,0 +1,7 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "src/hooks/AuthHooks";
export const PublicRoute = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? <Navigate to="/d" /> : <Outlet />;
};

View File

@ -1,72 +1,43 @@
import axios from "axios"; import axios from "axios";
import axiosInstance from "./configs/AxiosConfig"; import { axiosInstance } from "./AxiosConfig";
const baseURL = import.meta.env.VITE_BASE_URL;
// Function for user login // Function for user login
const apiUserLogin = data => { export const apiUserLogin = (data) => {
return axiosInstance return axiosInstance
.post("token/obtain/", data) .post("token/obtain/", data)
.then(response => { .then((response) => response)
console.log(response.statusText); .catch((error) => {
return response; throw error;
})
.catch(error => {
console.log("apiUserLogin error: ", error);
return error;
}); });
}; };
// Function for user logout // Function for user logout
const apiUserLogout = () => { export const apiUserLogout = () => {
axiosInstance.defaults.headers["Authorization"] = ""; // Clear authorization header axiosInstance.defaults.headers["Authorization"] = ""; // Clear authorization header
localStorage.removeItem("access_token"); // Remove access token localStorage.removeItem("access_token"); // Remove access token
localStorage.removeItem("refresh_token"); // Remove refresh token localStorage.removeItem("refresh_token"); // Remove refresh token
}; };
// Function for Google login // Function for Google login
const googleLogin = async token => { export const googleLogin = async (token) => {
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
let res = await axios.post("http://localhost:8000/api/auth/google/", { let res = await axios.post(`${baseURL}auth/google/`, {
code: token, code: token,
}); });
// console.log('service google login res: ', res); // console.log('service google login res: ', res);
return await res; return await res;
}; };
// Function to get 'hello' data
const getGreeting = () => {
return axiosInstance
.get("hello")
.then(response => {
return response;
})
.catch(error => {
return error;
});
};
const config = {
headers: {
"Content-Type": "application/json",
},
};
// Function to register // Function to register
const createUser = async formData => { export const createUser = async (formData) => {
try { try {
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
const resposne = axios.post("http://localhost:8000/api/user/create/", formData); const response = await axios.post(`${baseURL}user/create/`, formData);
// const response = await axiosInstance.post('/user/create/', formData);
return response.data; return response.data;
} catch (error) { } catch (e) {
throw error; console.error("Error in createUser function:", e);
throw e;
} }
}; };
// Export the functions and Axios instance
export default {
apiUserLogin,
apiUserLogout,
getGreeting: getGreeting,
googleLogin,
createUser,
};

View File

@ -0,0 +1,53 @@
import axios from "axios";
import { redirect } from "react-router-dom";
const baseURL = import.meta.env.VITE_BASE_URL;
export const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.request.use((config) => {
const access_token = localStorage.getItem("access_token");
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
});
// handling token refresh on 401 Unauthorized errors
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
const refresh_token = localStorage.getItem("refresh_token");
// Check if the error is due to 401 and a refresh token is available
if (error.response && error.response.status === 401) {
if (refresh_token) {
return axiosInstance
.post("/token/refresh/", { refresh: refresh_token })
.then((response) => {
localStorage.setItem("access_token", response.data.access);
axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access;
originalRequest.headers["Authorization"] = "Bearer " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log("Interceptors error: ", err);
});
} else {
redirect("/login");
}
}
return Promise.reject(error);
}
);

View File

@ -1,21 +1,21 @@
import axiosInstance from "./configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
const baseURL = ""; const baseURL = import.meta.env.VITE_BASE_URL;
export const createTask = (endpoint, data) => { export const createTask = (endpoint, data) => {
return axiosInstance return axiosInstance
.post(`${baseURL}${endpoint}/`, data) .post(`${baseURL}${endpoint}/`, data)
.then(response => response.data) .then((response) => response.data)
.catch(error => { .catch((error) => {
throw error; throw error;
}); });
}; };
export const readTasks = endpoint => { export const readTasks = (endpoint) => {
return axiosInstance return axiosInstance
.get(`${baseURL}${endpoint}/`) .get(`${baseURL}${endpoint}/`)
.then(response => response.data) .then((response) => response.data)
.catch(error => { .catch((error) => {
throw error; throw error;
}); });
}; };
@ -23,8 +23,8 @@ export const readTasks = endpoint => {
export const readTaskByID = (endpoint, id) => { export const readTaskByID = (endpoint, id) => {
return axiosInstance return axiosInstance
.get(`${baseURL}${endpoint}/${id}/`) .get(`${baseURL}${endpoint}/${id}/`)
.then(response => response.data) .then((response) => response.data)
.catch(error => { .catch((error) => {
throw error; throw error;
}); });
}; };
@ -32,8 +32,8 @@ export const readTaskByID = (endpoint, id) => {
export const updateTask = (endpoint, id, data) => { export const updateTask = (endpoint, id, data) => {
return axiosInstance return axiosInstance
.put(`${baseURL}${endpoint}/${id}/`, data) .put(`${baseURL}${endpoint}/${id}/`, data)
.then(response => response.data) .then((response) => response.data)
.catch(error => { .catch((error) => {
throw error; throw error;
}); });
}; };
@ -41,16 +41,16 @@ export const updateTask = (endpoint, id, data) => {
export const deleteTask = (endpoint, id) => { export const deleteTask = (endpoint, id) => {
return axiosInstance return axiosInstance
.delete(`${baseURL}${endpoint}/${id}/`) .delete(`${baseURL}${endpoint}/${id}/`)
.then(response => response.data) .then((response) => response.data)
.catch(error => { .catch((error) => {
throw error; throw error;
}); });
}; };
// Create // Create
export const createTodoTask = data => createTask("todo", data); export const createTodoTask = (data) => createTask("todo", data);
export const createRecurrenceTask = data => createTask("daily", data); export const createRecurrenceTask = (data) => createTask("daily", data);
export const createHabitTask = data => createTask("habit", data); export const createHabitTask = (data) => createTask("habit", data);
// Read // Read
export const readTodoTasks = () => readTasks("todo"); export const readTodoTasks = () => readTasks("todo");
@ -58,9 +58,9 @@ export const readRecurrenceTasks = () => readTasks("daily");
export const readHabitTasks = () => readTasks("habit"); export const readHabitTasks = () => readTasks("habit");
// Read by ID // Read by ID
export const readTodoTaskByID = id => readTaskByID("todo", id); export const readTodoTaskByID = (id) => readTaskByID("todo", id);
export const readRecurrenceTaskByID = id => readTaskByID("daily", id); export const readRecurrenceTaskByID = (id) => readTaskByID("daily", id);
export const readHabitTaskByID = id => readTaskByID("habit", id); export const readHabitTaskByID = (id) => readTaskByID("habit", id);
// Update // Update
export const updateTodoTask = (id, data) => updateTask("todo", id, data); export const updateTodoTask = (id, data) => updateTask("todo", id, data);
@ -68,6 +68,6 @@ export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
export const updateHabitTask = (id, data) => updateTask("habit", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data);
// Delete // Delete
export const deleteTodoTask = id => deleteTask("todo", id); export const deleteTodoTask = (id) => deleteTask("todo", id);
export const deleteRecurrenceTask = id => deleteTask("daily", id); export const deleteRecurrenceTask = (id) => deleteTask("daily", id);
export const deleteHabitTask = id => deleteTask("habit", id); export const deleteHabitTask = (id) => deleteTask("habit", id);

View File

@ -1,8 +1,10 @@
import axios from "axios"; import axios from "axios";
const ApiUpdateUserProfile = async formData => { const baseURL = import.meta.env.VITE_BASE_URL;
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(`${baseURL}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",

View File

@ -1,45 +0,0 @@
import axios from "axios";
import { redirect } from "react-router-dom";
const axiosInstance = axios.create({
baseURL: "http://127.0.0.1:8000/api/",
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
// handling token refresh on 401 Unauthorized errors
axiosInstance.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
const refresh_token = localStorage.getItem("refresh_token");
// 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"
) {
return axiosInstance
.post("/token/refresh/", { refresh: refresh_token })
.then(response => {
localStorage.setItem("access_token", response.data.access);
axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access;
originalRequest.headers["Authorization"] = "Bearer " + response.data.access;
return axiosInstance(originalRequest);
})
.catch(err => {
console.log("Interceptors error: ", err);
});
}
return Promise.reject(error);
}
);
export default axiosInstance;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,14 +1,14 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi"; import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi";
import { readTodoTasks } from "../../api/TaskApi"; import { readTodoTasks } from "../../api/TaskApi";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
function EachBlog({ name, colorCode, contentList, icon }) { function EachBlog({ name, colorCode, contentList, icon }) {
const [tasks, setTasks] = useState(contentList); const [tasks, setTasks] = useState(contentList);
const handleCheckboxChange = async index => { const handleCheckboxChange = async (index) => {
try { try {
setTasks(contentList) setTasks(contentList);
const updatedTasks = [...tasks]; const updatedTasks = [...tasks];
const taskId = updatedTasks[index].id; const taskId = updatedTasks[index].id;
@ -55,17 +55,17 @@ function EachBlog({ name, colorCode, contentList, icon }) {
); );
} }
function Eisenhower() { export function Eisenhower() {
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
useEffect(() => { useEffect(() => {
readTodoTasks() readTodoTasks()
.then(data => { .then((data) => {
console.log(data); console.log(data);
const contentList_ui = data.filter(task => task.priority === 1); const contentList_ui = data.filter((task) => task.priority === 1);
const contentList_uni = data.filter(task => task.priority === 2); const contentList_uni = data.filter((task) => task.priority === 2);
const contentList_nui = data.filter(task => task.priority === 3); const contentList_nui = data.filter((task) => task.priority === 3);
const contentList_nuni = data.filter(task => task.priority === 4); const contentList_nuni = data.filter((task) => task.priority === 4);
setTasks({ setTasks({
contentList_ui, contentList_ui,
@ -74,7 +74,7 @@ function Eisenhower() {
contentList_nuni, contentList_nuni,
}); });
}) })
.catch(error => console.error("Error fetching tasks:", error)); .catch((error) => console.error("Error fetching tasks:", error));
}, []); }, []);
return ( return (
@ -108,5 +108,3 @@ function Eisenhower() {
</div> </div>
); );
} }
export default Eisenhower;

View File

@ -0,0 +1,84 @@
import { useCallback } from "react";
import Particles from "react-tsparticles";
import { loadFull } from "tsparticles";
export function FloatingParticles() {
const particlesInit = useCallback(async (engine) => {
await loadFull(engine);
}, []);
return (
<div style={{ width: "0%", height: "100vh" }}>
<Particles
id="particles"
init={particlesInit}
className="-z-10"
options={{
fpsLimit: 240,
smooth: true,
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: "#4f46e5",
},
links: {
color: "#818cf8",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
move: {
direction: "none",
enable: true,
outModes: {
default: "bounce",
},
random: false,
speed: 2,
straight: false,
},
number: {
density: {
enable: true,
area: 800,
},
value: 40,
},
opacity: {
value: 0.5,
},
shape: {
type: "square",
},
size: {
value: { min: 4, max: 5 },
},
},
detectRetina: true,
}}
/>
</div>
);
}

View File

@ -1,27 +1,19 @@
import React, { useEffect, useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate, redirect } 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 axiosapi from "../../api/AuthenticationApi";
import { useAuth } from "../../hooks/authentication/IsAuthenticated";
import { FcGoogle } from "react-icons/fc"; import { FcGoogle } from "react-icons/fc";
import { useAuth } from "src/hooks/AuthHooks";
import { FloatingParticles } from "../FlaotingParticles";
import { NavPreLogin } from "../navigations/NavPreLogin";
import { apiUserLogin, googleLogin } from "src/api/AuthenticationApi";
export function LoginPage() {
function LoginPage() { const { setIsAuthenticated } = useAuth();
const Navigate = useNavigate(); const Navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth();
useEffect(() => {
if (!refreshAccessToken()) {
Navigate("/");
}
}, []);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const handleEmailChange = (event) => { const handleEmailChange = (event) => {
setEmail(event.target.value); setEmail(event.target.value);
@ -30,28 +22,23 @@ function LoginPage() {
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
axiosapi apiUserLogin({
.apiUserLogin({ email: email,
email: email, password: password,
password: password, })
})
.then((res) => { .then((res) => {
// 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;
setIsAuthenticated(true); setIsAuthenticated(true);
Navigate("/"); redirect("/");
}) })
.catch((err) => { .catch((err) => {
console.log("Login failed"); setError("Incorrect username or password");
console.log(err);
setIsAuthenticated(false);
}); });
}; };
@ -60,7 +47,7 @@ function LoginPage() {
redirect_uri: "postmessage", redirect_uri: "postmessage",
onSuccess: async (response) => { onSuccess: async (response) => {
try { try {
const loginResponse = await axiosapi.googleLogin(response.code); const loginResponse = await googleLogin(response.code);
if (loginResponse && loginResponse.data) { if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data; const { access_token, refresh_token } = loginResponse.data;
@ -71,157 +58,96 @@ function LoginPage() {
} }
} 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),
}); });
{
/* Particles Loader*/
}
const particlesInit = useCallback(async (engine) => {
console.log(engine);
await loadFull(engine);
}, []);
const particlesLoaded = useCallback(async (container) => {
console.log(container);
}, []);
return ( return (
<div <div>
data-theme="night" <NavPreLogin
className="h-screen flex items-center justify-center" text="Don't have account?"
> btn_text="Sign Up"
{/* Particles Container */} link="/signup"
<div style={{ width: "0%", height: "100vh" }}> />
<Particles <div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
id="particles" {/* Particles Container */}
init={particlesInit}
loaded={particlesLoaded} <FloatingParticles />
className="-z-10" {/* Login Box */}
options={{ <div className="flex items-center justify-center flex-1 z-50">
fpsLimit: 240, <div className="w-100 bg-white border-solid rounded-lg p-8 shadow space-y-4">
interactivity: { <h2 className="text-3xl font-bold">Log in to your account</h2>
events: { {/* Error Message */}
onClick: { {error && (
enable: true, <div role="alert" className="alert alert-error">
mode: "push", <svg
}, xmlns="http://www.w3.org/2000/svg"
onHover: { className="stroke-current shrink-0 h-6 w-6"
enable: true, fill="none"
mode: "repulse", viewBox="0 0 24 24"
}, >
resize: true, <path
}, strokeLinecap="round"
modes: { strokeLinejoin="round"
push: { strokeWidth="2"
quantity: 4, d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
}, />
repulse: { </svg>
distance: 200, <span>{error}</span>
duration: 0.4, </div>
}, )}
}, {/* Email Input */}
}, <div className="form-control ">
particles: { <label className="label" htmlFor="email">
color: { <p className="text-bold">
value: "#008000", Email<span className="text-red-500 text-bold">*</span>
}, </p>
links: { </label>
color: "#00ff00", <input
distance: 150, className="input"
enable: true, type="email"
opacity: 0.5, id="email"
width: 1, placeholder="Enter your email"
}, value={email}
move: { onChange={handleEmailChange}
direction: "none", />
enable: true, </div>
outModes: { {/* Password Input */}
default: "bounce", <div className="form-control">
}, <label className="label" htmlFor="password">
random: false, <p className="text-bold">
speed: 4, Password<span className="text-red-500 text-bold">*</span>
straight: false, </p>
}, </label>
number: { <input
density: { className="input"
enable: true, type="password"
area: 800, id="password"
}, placeholder="Enter your password"
value: 50, value={password}
}, onChange={handlePasswordChange}
opacity: { />
value: 0.5, </div>
}, {/* Login Button */}
shape: { <button
type: "circle", className="btn bg-blue-700 hover:bg-blue-900 w-full text-white font-bold"
}, onClick={handleSubmit}
size: { >
value: { min: 4, max: 5 }, Login
}, </button>
}, <div className="divider">OR</div>
detectRetina: true, {/* Login with Google Button */}
}} <button
/> className="btn bg-gray-200 btn-outline w-full "
</div> onClick={() => googleLoginImplicit()}
{/* Login Box */} >
<div className="w-1/4 flex items-center justify-center"> <FcGoogle className="rounded-full bg-white" />
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4 z-10"> Login with Google
<h2 className="text-3xl font-bold text-center">Login</h2> </button>
{/* 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={handleEmailChange}
/>
</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={handlePasswordChange}
/>
</div>
{/* Login Button */}
<button className="btn btn-success w-full " onClick={handleSubmit}>
Login
</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>
{/* Forgot Password Link */}
<div className="justify-left">
<a href="#" className="text-blue-500 text-sm text-left">
Forgot your password?
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export default LoginPage;

View File

@ -1,31 +1,15 @@
import React, { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
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 { FcGoogle } from "react-icons/fc";
import { useGoogleLogin } from "@react-oauth/google"; import { useGoogleLogin } from "@react-oauth/google";
import { NavPreLogin } from "../navigations/NavPreLogin";
import { useAuth } from "src/hooks/AuthHooks";
import { createUser, googleLogin } from "src/api/AuthenticationApi";
import { FloatingParticles } from "../FlaotingParticles";
export function SignUp() {
function Copyright(props) {
return (
<div className="text-center text-sm text-gray-500" {...props}>
{"Copyright © "}
<a
href="https://github.com/TurTaskProject/TurTaskWeb"
className="text-blue-500 hover:underline"
>
TurTask
</a>{" "}
{new Date().getFullYear()}
{"."}
</div>
);
}
export default function SignUp() {
const Navigate = useNavigate(); const Navigate = useNavigate();
const { setIsAuthenticated } = useAuth();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: "",
@ -40,47 +24,48 @@ export default function SignUp() {
setIsSubmitting(true); setIsSubmitting(true);
setError(null); setError(null);
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
try { try {
axiosapi.createUser(formData); const data = await createUser(formData);
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
await delay(200);
setIsAuthenticated(true);
Navigate("/");
} 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");
}; };
const handleChange = (e) => { const handleEmailChange = (e) => {
const { name, value } = e.target; setFormData({ ...formData, email: e.target.value });
setFormData({ ...formData, [name]: value });
console.log(formData);
}; };
{
/* Particles Loader*/
}
const particlesInit = useCallback(async (engine) => {
console.log(engine);
await loadFull(engine);
}, []);
const particlesLoaded = useCallback(async (container) => { const handleUsernameChange = (e) => {
console.log(container); setFormData({ ...formData, username: e.target.value });
}, []); };
const handlePasswordChange = (e) => {
setFormData({ ...formData, password: e.target.value });
};
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 googleLogin(response.code);
if (loginResponse && loginResponse.data) { if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data; const { access_token, refresh_token } = loginResponse.data;
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); setIsAuthenticated(true);
Navigate("/"); Navigate("/profile");
} }
} catch (error) { } catch (error) {
console.error("Error with the POST request:", error); console.error("Error with the POST request:", error);
@ -91,153 +76,83 @@ export default function SignUp() {
}); });
return ( return (
<div <div>
data-theme="night" <NavPreLogin
className="h-screen flex items-center justify-center" text="Already have an account?"
> btn_text="Log In"
{/* Particles Container */} link="/login"
<div style={{ width: "0%", height: "100vh" }}> />
<Particles <div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
id="particles" <FloatingParticles />
init={particlesInit} <div className="w-1/4 h-1 flex items-center justify-center z-10">
loaded={particlesLoaded} <div className="w-96 bg-white rounded-lg p-8 space-y-4 z-10">
className="-z-10" {/* Register Form */}
options={{ <h2 className="text-3xl font-bold text-center">Signup</h2>
fpsLimit: 240, {/* Email Input */}
interactivity: { <div className="form-control ">
events: { <label className="label" htmlFor="email">
onClick: { <p className="text-bold">
enable: true, Email<span className="text-red-500 text-bold">*</span>
mode: "push", </p>
}, </label>
onHover: { <input
enable: true, className="input"
mode: "repulse", type="email"
}, id="email"
resize: true, placeholder="Enter your email"
}, onChange={handleEmailChange}
modes: { />
push: { </div>
quantity: 4, {/* Username Input */}
}, <div className="form-control">
repulse: { <label className="label" htmlFor="Username">
distance: 200, <p className="text-bold">
duration: 0.4, Username<span className="text-red-500 text-bold">*</span>
}, </p>
}, </label>
}, <input
particles: { className="input"
color: { type="text"
value: "#023020", id="Username"
}, placeholder="Enter your username"
links: { onChange={handleUsernameChange}
color: "#228B22", />
distance: 150, </div>
enable: true, {/* Password Input */}
opacity: 1, <div className="form-control">
width: 1, <label className="label" htmlFor="password">
}, <p className="text-bold">
move: { Password<span className="text-red-500 text-bold">*</span>
direction: "none", </p>
enable: true, </label>
outModes: { <input
default: "bounce", className="input"
}, type="password"
random: false, id="password"
speed: 4, placeholder="Enter your password"
straight: false, onChange={handlePasswordChange}
}, />
number: { </div>
density: { <br></br>
enable: true,
area: 800,
},
value: 50,
},
opacity: {
value: 0.6,
},
shape: {
type: "circle",
},
size: {
value: { min: 6, max: 8 },
},
},
detectRetina: true,
}}
/>
</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>
{/* Signups Button */} {/* Signups Button */}
<button className="btn btn-success w-full " onClick={handleSubmit}> <button className="btn btn-success w-full " onClick={handleSubmit}>
Signup Signup
</button> </button>
<div className="divider">OR</div> <div className="divider">OR</div>
{/* Login with Google Button */} {/* Login with Google Button */}
<button <button
className="btn btn-outline btn-secondary w-full " className="btn btn-outline btn-secondary w-full "
onClick={() => googleLoginImplicit()} onClick={() => googleLoginImplicit()}
> >
<FcGoogle className="rounded-full bg-white"/>Login with Google <FcGoogle className="rounded-full bg-white" />
</button> Login with Google
{/* Already have an account? */} </button>
<div className="text-blue-500 flex justify-center text-sm"> {/* Already have an account? */}
<a href="login"> <div className="text-blue-500 flex justify-center text-sm">
Already have an account? <a href="login">Already have an account?</a>
</a> </div>
</div> </div>
<Copyright />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,37 +0,0 @@
import axios from "axios";
async function refreshAccessToken() {
const refresh_token = localStorage.getItem("refresh_token");
const access_token = localStorage.getItem("access_token");
if (access_token) {
return true;
}
if (!refresh_token) {
return false;
}
const refreshUrl = "http://127.0.0.1:8000/api/token/refresh/";
try {
const response = await axios.post(refreshUrl, { refresh: refresh_token });
if (response.status === 200) {
// Successful refresh - save the new access token and refresh token
const newAccessToken = response.data.access;
const newRefreshToken = response.data.refresh;
localStorage.setItem("access_token", newAccessToken);
localStorage.setItem("refresh_token", newRefreshToken);
return true;
} else {
return false;
}
} catch (error) {
return false;
}
}
export default refreshAccessToken;

View File

@ -1,9 +1,9 @@
import { readTodoTasks } from "../../api/TaskApi"; import { readTodoTasks } from "src/api/TaskApi";
let eventGuid = 0; let eventGuid = 0;
const mapResponseToEvents = response => { const mapResponseToEvents = (response) => {
return response.map(item => ({ return response.map((item) => ({
id: item.id, id: item.id,
title: item.title, title: item.title,
start: item.start_event, start: item.start_event,

View File

@ -1,13 +1,13 @@
import React, { useState } from "react"; import React 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 axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
export default class Calendar extends React.Component { export class Calendar extends React.Component {
state = { state = {
weekendsVisible: true, weekendsVisible: true,
currentEvents: [], currentEvents: [],
@ -83,7 +83,7 @@ export default class Calendar extends React.Component {
}); });
}; };
handleDateSelect = selectInfo => { handleDateSelect = (selectInfo) => {
let title = prompt("Please enter a new title for your event"); let title = prompt("Please enter a new title for your event");
let calendarApi = selectInfo.view.calendar; let calendarApi = selectInfo.view.calendar;
@ -100,20 +100,20 @@ export default class Calendar extends React.Component {
} }
}; };
handleEventClick = clickInfo => { handleEventClick = (clickInfo) => {
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
axiosInstance axiosInstance
.delete(`todo/${clickInfo.event.id}/`) .delete(`todo/${clickInfo.event.id}/`)
.then(response => { .then((response) => {
clickInfo.event.remove(); clickInfo.event.remove();
}) })
.catch(error => { .catch((error) => {
console.error("Error deleting Task:", error); console.error("Error deleting Task:", error);
}); });
} }
}; };
handleEvents = events => { handleEvents = (events) => {
this.setState({ this.setState({
currentEvents: events, currentEvents: events,
}); });

View File

@ -1,103 +1,36 @@
import { AreaChart, Title } from "@tremor/react"; import { AreaChart, Title } from "@tremor/react";
import React from "react"; import { useState, useEffect } from "react";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
const fetchAreaChartData = async () => {
let res = await axiosInstance.get("/dashboard/weekly/");
console.log(res.data);
// const areaChartData = [
// {
// date: "Mon",
// "This Week": res.data[0]["This Week"],
// "Last Week": res.data[0]["Last Week"],
// },
// {
// date: "Tue",
// "This Week": res.data[1]["This Week"],
// "Last Week": res.data[1]["Last Week"],
// },
// {
// date: "Wed",
// "This Week": res.data[2]["This Week"],
// "Last Week": res.data[2]["Last Week"],
// },
// {
// date: "Th",
// "This Week": res.data[3]["This Week"],
// "Last Week": res.data[3]["Last Week"],
// },
// {
// date: "Fri",
// "This Week": res.data[4]["This Week"],
// "Last Week": res.data[4]["Last Week"],
// },
// {
// date: "Sat",
// "This Week": res.data[5]["This Week"],
// "Last Week": res.data[5]["Last Week"],
// },
// {
// date: "Sun",
// "This Week": res.data[6]["This Week"],
// "Last Week": res.data[6]["Last Week"],
// },
// ];
const areaChartData = [
{
date: "Mon",
"This Week": 1,
"Last Week": 2,
},
{
date: "Tue",
"This Week": 5,
"Last Week": 2,
},
{
date: "Wed",
"This Week": 7,
"Last Week": 9,
},
{
date: "Th",
"This Week": 10,
"Last Week": 3,
},
{
date: "Fri",
"This Week": 5,
"Last Week": 1,
},
{
date: "Sat",
"This Week": 7,
"Last Week": 8,
},
{
date: "Sun",
"This Week": 3,
"Last Week": 8,
},
];
return areaChartData;
}
const areaChartDataArray = await fetchAreaChartData();
export const AreaChartGraph = () => { export const AreaChartGraph = () => {
const [value, setValue] = React.useState(null); const [areaChartDataArray, setAreaChartDataArray] = useState([]);
return (
<> useEffect(() => {
<Title>Number of tasks statistics vs. last week</Title> const fetchAreaChartData = async () => {
<AreaChart try {
className="mt-6" const response = await axiosInstance.get("/dashboard/weekly/");
data={areaChartDataArray} const areaChartData = response.data;
index="date" setAreaChartDataArray(areaChartData);
categories={["This Week", "Last Week"]} } catch (error) {
colors={["neutral", "indigo"]} console.error("Error fetching area chart data:", error);
yAxisWidth={30} }
onValueChange={(v) => setValue(v)} };
showAnimation
/> fetchAreaChartData();
</> }, []);
);
}; return (
<>
<Title>Number of tasks statistics vs. last week</Title>
<AreaChart
className="mt-6"
data={areaChartDataArray}
index="date"
categories={["This Week", "Last Week"]}
colors={["neutral", "indigo"]}
yAxisWidth={30}
showAnimation
/>
</>
);
};

View File

@ -1,89 +1,24 @@
import { BarChart, Title } from "@tremor/react"; import { BarChart, Title } from "@tremor/react";
import React from "react"; import { useState, useEffect } from "react";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
const fetchBarChartData = async () => {
let res = await axiosInstance.get("/dashboard/weekly/");
console.log(res.data);
// const barchartData = [
// {
// date: "Mon",
// "This Week": res.data[0]["Completed This Week"],
// "Last Week": res.data[0]["Completed Last Week"],
// },
// {
// date: "Tue",
// "This Week": res.data[1]["Completed This Week"],
// "Last Week": res.data[1]["Completed Last Week"],
// },
// {
// date: "Wed",
// "This Week": res.data[2]["Completed This Week"],
// "Last Week": res.data[2]["Completed Last Week"],
// },
// {
// date: "Th",
// "This Week": res.data[3]["Completed This Week"],
// "Last Week": res.data[3]["Completed Last Week"],
// },
// {
// date: "Fri",
// "This Week": res.data[4]["Completed This Week"],
// "Last Week": res.data[4]["Completed Last Week"],
// },
// {
// date: "Sat",
// "This Week": res.data[5]["Completed This Week"],
// "Last Week": res.data[5]["Completed Last Week"],
// },
// {
// date: "Sun",
// "This Week": res.data[6]["Completed This Week"],
// "Last Week": res.data[6]["Completed Last Week"],
// },
// ];
const barchartData = [
{
date: "Mon",
"This Week": 1,
"Last Week": 2,
},
{
date: "Tue",
"This Week": 5,
"Last Week": 2,
},
{
date: "Wed",
"This Week": 7,
"Last Week": 9,
},
{
date: "Th",
"This Week": 10,
"Last Week": 3,
},
{
date: "Fri",
"This Week": 5,
"Last Week": 1,
},
{
date: "Sat",
"This Week": 7,
"Last Week": 8,
},
{
date: "Sun",
"This Week": 3,
"Last Week": 8,
},
];
return barchartData;
};
const barchartDataArray = await fetchBarChartData();
export const BarChartGraph = () => { export const BarChartGraph = () => {
const [value, setValue] = React.useState(null); const [barchartDataArray, setBarChartDataArray] = useState([]);
useEffect(() => {
const fetchAreaChartData = async () => {
try {
const response = await axiosInstance.get("/dashboard/weekly/");
const barchartDataArray = response.data;
setBarChartDataArray(barchartDataArray);
} catch (error) {
console.error("Error fetching area chart data:", error);
}
};
fetchAreaChartData();
}, []);
return ( return (
<> <>
<Title>Task completed statistics vs. last week</Title> <Title>Task completed statistics vs. last week</Title>
@ -94,7 +29,6 @@ export const BarChartGraph = () => {
categories={["This Week", "Last Week"]} categories={["This Week", "Last Week"]}
colors={["neutral", "indigo"]} colors={["neutral", "indigo"]}
yAxisWidth={30} yAxisWidth={30}
onValueChange={(v) => setValue(v)}
showAnimation showAnimation
/> />
</> </>

View File

@ -1,40 +1,37 @@
import { DonutChart } from "@tremor/react"; import { DonutChart } from "@tremor/react";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
import { useState, useEffect } from "react";
const fetchDonutData = async () => { export function DonutChartGraph() {
try { const [donutData, setDonutData] = useState([]);
let res = await axiosInstance.get("/dashboard/stats/");
// let todoCount = res.data.todo_count; useEffect(() => {
// let recurrenceCount = res.data.recurrence_count; const fetchDonutData = async () => {
let todoCount = 10; try {
let recurrenceCount = 15; const response = await axiosInstance.get("/dashboard/stats/");
if (todoCount === undefined) { const todoCount = response.data.todo_count || 0;
todoCount = 0; const recurrenceCount = response.data.recurrence_count || 0;
}
if (recurrenceCount === undefined) { const donutData = [
recurrenceCount = 0; { name: "Todo", count: todoCount },
} { name: "Recurrence", count: recurrenceCount },
const donutData = [ ];
{ name: "Todo", count: todoCount },
{ name: "Recurrence", count: recurrenceCount }, setDonutData(donutData);
]; } catch (error) {
return donutData; console.error("Error fetching donut data:", error);
} catch (error) { }
console.error("Error fetching donut data:", error); };
return []; fetchDonutData();
} }, []);
};
const donutDataArray = await fetchDonutData();
export default function DonutChartGraph() {
return ( return (
<DonutChart <DonutChart
className="mt-6" className="mt-6"
data={donutDataArray} data={donutData}
category="count" category="count"
index="name" index="name"
colors={["rose", "yellow", "orange"]} colors={["rose", "yellow", "orange"]}
onValueChange={(v) => setValue(v)}
showAnimation showAnimation
radius={25} radius={25}
/> />

View File

@ -1,42 +1,57 @@
import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react"; import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react";
import React from "react"; import { useEffect, useState } from "react";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
const fetchKpiCardData = async () => { export function KpiCard() {
let res = await axiosInstance.get("/dashboard/stats/"); const [kpiCardData, setKpiCardData] = useState({
// let completedThisWeek = res.data["completed_this_week"]; completedThisWeek: 0,
// let completedLastWeek = res.data["completed_last_week"]; completedLastWeek: 0,
let completedThisWeek = 4; incOrdec: undefined,
let completedLastWeek = 23; percentage: 0,
let percentage = (completedThisWeek / completedLastWeek)*100; });
let incOrdec = undefined;
if (completedThisWeek <= completedLastWeek) {
incOrdec = "moderateDecrease";
}
if (completedThisWeek > completedLastWeek) {
incOrdec = "moderateIncrease";
}
return {completedThisWeek, completedLastWeek, incOrdec, percentage};
}
const {kpiCardDataArray, completedThisWeek ,completedLastWeek, incOrdec, percentage} = await fetchKpiCardData(); useEffect(() => {
const fetchKpiCardData = async () => {
try {
const response = await axiosInstance.get("/dashboard/stats/");
const completedThisWeek = response.data.completed_this_week || 0;
const completedLastWeek = response.data.completed_last_week || 0;
const percentage = (completedThisWeek / completedLastWeek) * 100;
let incOrdec = undefined;
if (completedThisWeek <= completedLastWeek) {
incOrdec = "moderateDecrease";
}
if (completedThisWeek > completedLastWeek) {
incOrdec = "moderateIncrease";
}
setKpiCardData({
completedThisWeek,
completedLastWeek,
incOrdec,
percentage,
});
} catch (error) {
console.error("Error fetching KPI card data:", error);
}
};
fetchKpiCardData();
}, []);
export default function KpiCard() {
return ( return (
<Card className="max-w-lg mx-auto"> <Card className="max-w-lg mx-auto">
<Flex alignItems="start"> <Flex alignItems="start">
<div> <div>
<Metric>{completedThisWeek}</Metric> <Metric>{kpiCardData.completedThisWeek}</Metric>
</div> </div>
<BadgeDelta deltaType={incOrdec}>{percentage.toFixed(0)}%</BadgeDelta> <BadgeDelta deltaType={kpiCardData.incOrdec}>{kpiCardData.percentage.toFixed(0)}%</BadgeDelta>
</Flex> </Flex>
<Flex className="mt-4"> <Flex className="mt-4">
<Text className="truncate">vs. {completedLastWeek} (last week)</Text> <Text className="truncate">vs. {kpiCardData.completedLastWeek} (last week)</Text>
</Flex> </Flex>
<ProgressBar value={percentage} className="mt-2" /> <ProgressBar value={kpiCardData.percentage} className="mt-2" />
</Card> </Card>
); );
} }

View File

@ -1,38 +1,39 @@
import { Card, Flex, ProgressCircle, Text, } from "@tremor/react"; import { Card, Flex, ProgressCircle } from "@tremor/react";
import React from "react"; import { useState, useEffect } from "react";
import axiosInstance from "../../api/configs/AxiosConfig"; import { axiosInstance } from "src/api/AxiosConfig";
const fetchProgressData = async () => { export function ProgressCircleChart() {
try { const [progressData, setProgressData] = useState(0);
let res = await axiosInstance.get("/dashboard/stats/");
// let completedLastWeek = res.data.completed_last_week; useEffect(() => {
// let assignLastWeek = res.data.tasks_assigned_last_week; const fetchProgressData = async () => {
let completedLastWeek = 15; try {
let assignLastWeek = 35; const response = await axiosInstance.get("/dashboard/stats/");
if (completedLastWeek === undefined) { let completedLastWeek = response.data.completed_last_week || 0;
completedLastWeek = 0; let assignLastWeek = response.data.tasks_assigned_last_week || 0;
}
if (assignLastWeek === undefined) { if (completedLastWeek === undefined) {
assignLastWeek = 0; completedLastWeek = 0;
} }
return (completedLastWeek / assignLastWeek) * 100; if (assignLastWeek === undefined) {
} catch (error) { assignLastWeek = 0;
console.error("Error fetching progress data:", error); }
return 0;
} const progress = (completedLastWeek / assignLastWeek) * 100;
};
setProgressData(progress);
} catch (error) {
console.error("Error fetching progress data:", error);
}
};
fetchProgressData();
}, []);
const progressData = await fetchProgressData();
export default function ProgressCircleChart() {
return ( return (
<Card className="max-w-lg mx-auto"> <Card className="max-w-lg mx-auto">
<Flex className="flex-col items-center"> <Flex className="flex-col items-center">
<ProgressCircle <ProgressCircle className="mt-6" value={progressData} size={200} strokeWidth={10} radius={60} color="indigo">
className="mt-6" value={progressData}
size={200}
strokeWidth={10}
radius={60}
color="indigo">
<span className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center text-sm text-indigo-500 font-medium"> <span className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center text-sm text-indigo-500 font-medium">
{progressData.toFixed(0)} % {progressData.toFixed(0)} %
</span> </span>

View File

@ -1,46 +1,22 @@
import { import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react";
Card, import { KpiCard } from "./KpiCard";
Grid,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
Text,
Title,
Legend,
DateRangePicker,
} from "@tremor/react";
import KpiCard from "./kpiCard";
import { BarChartGraph } from "./Barchart"; import { BarChartGraph } from "./Barchart";
import DonutChartGraph from "./DonutChart"; import { DonutChartGraph } from "./DonutChart";
import { AreaChartGraph } from "./Areachart"; import { AreaChartGraph } from "./Areachart";
import ProgressCircleChart from "./ProgressCircle"; import { ProgressCircleChart } from "./ProgressCircle";
import { useState } from "react"; import { useState } from "react";
const valueFormatter = (number) => export function Dashboard() {
`$ ${new Intl.NumberFormat("us").format(number).toString()}`;
export default function Dashboard() {
const [value, setValue] = useState({ const [value, setValue] = useState({
from: new Date(2021, 0, 1), from: new Date(2021, 0, 1),
to: new Date(2023, 0, 7), to: new Date(2023, 0, 7),
}); });
console.log(value);
return ( return (
<div className="flex flex-col p-12"> <div className="flex flex-col p-12">
<div> <div>
<Title>Dashboard</Title> <Title>Dashboard</Title>
<Text>All of your progress will be shown right here.</Text> <Text>All of your progress will be shown right here.</Text>
<br /> <br />
<Text className="mr-3">Select Date Range:</Text>
<DateRangePicker
value={value}
onChange={setValue}
className="inline-block"
maxDate={new Date()}
>
</DateRangePicker>
</div> </div>
<div> <div>
@ -64,8 +40,7 @@ export default function Dashboard() {
<Legend <Legend
className="mt-3 mx-auto w-1/2" className="mt-3 mx-auto w-1/2"
categories={["Completed Tasks", "Assigned Tasks"]} categories={["Completed Tasks", "Assigned Tasks"]}
colors={["indigo"]} colors={["indigo"]}></Legend>
></Legend>
</Card> </Card>
<Card> <Card>
<BarChartGraph /> <BarChartGraph />

View File

@ -3,13 +3,13 @@ import { BsFillTrashFill } from "react-icons/bs";
import { AiOutlinePlusCircle } from "react-icons/ai"; 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 TaskCard from "./taskCard"; import { TaskCard } from "./taskCard";
function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) { export function ColumnContainer({ 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 { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
@ -78,12 +78,12 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks
<input <input
className="bg-gray-200 focus:border-blue-500 border rounded-md 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);
}} }}
@ -109,7 +109,7 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks
{/* Column task container */} {/* Column task container */}
<div className="flex flex-grow flex-col gap-2 p-1 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 key={task.id} task={task} deleteTask={deleteTask} updateTask={updateTask} /> <TaskCard key={task.id} task={task} deleteTask={deleteTask} updateTask={updateTask} />
))} ))}
</SortableContext> </SortableContext>
@ -126,5 +126,3 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks
</div> </div>
); );
} }
export default ColumnContainer;

View File

@ -1,6 +1,6 @@
import ColumnContainer from "./columnContainer"; import { ColumnContainer } from "./columnContainer";
function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) { export function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, tasks, deleteTask, updateTask }) {
return ( return (
<div className="card bg-[#f1f2f4] shadow p-1 my-2 border-2"> <div className="card bg-[#f1f2f4] shadow p-1 my-2 border-2">
<ColumnContainer <ColumnContainer
@ -15,5 +15,3 @@ function ColumnContainerCard({ column, deleteColumn, updateColumn, createTask, t
</div> </div>
); );
} }
export default ColumnContainerCard;

View File

@ -1,15 +1,14 @@
import { useMemo, useState, useEffect } from "react"; import { useMemo, useState, useEffect } from "react";
import ColumnContainerCard from "./columnContainerWrapper"; import { ColumnContainerCard } from "./columnContainerWrapper";
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { 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"; import { axiosInstance } from "src/api/AxiosConfig";
import axiosInstance from "../../api/configs/AxiosConfig";
function KanbanBoard() { export function KanbanBoard() {
const [columns, setColumns] = useState([]); const [columns, setColumns] = useState([]);
const columnsId = useMemo(() => columns.map(col => col.id), [columns]); const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
const [boardId, setBoardData] = useState(); const [boardId, setBoardData] = useState();
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
@ -26,54 +25,19 @@ function KanbanBoard() {
}) })
); );
// Example
// {
// "id": 95,
// "title": "Test Todo",
// "notes": "Test TodoTest TodoTest Todo",
// "importance": 1,
// "difficulty": 1,
// "challenge": false,
// "fromSystem": false,
// "creation_date": "2023-11-20T19:50:16.369308Z",
// "last_update": "2023-11-20T19:50:16.369308Z",
// "is_active": true,
// "is_full_day_event": false,
// "start_event": "2023-11-20T19:49:49Z",
// "end_event": "2023-11-23T18:00:00Z",
// "google_calendar_id": null,
// "completed": true,
// "completion_date": "2023-11-20T19:50:16.369308Z",
// "priority": 3,
// "user": 1,
// "list_board": 1,
// "tags": []
// }
// ]
// [
// {
// "id": 8,
// "name": "test",
// "position": 2,
// "board": 3
// }
// ]
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const tasksResponse = await axiosInstance.get("/todo"); const tasksResponse = await axiosInstance.get("/todo");
// Transform // Transform
const transformedTasks = tasksResponse.data.map(task => ({ const transformedTasks = tasksResponse.data.map((task) => ({
id: task.id, id: task.id,
columnId: task.list_board, columnId: task.list_board,
content: task.title, content: task.title,
difficulty: task.difficulty, difficulty: task.difficulty,
notes: task.notes, notes: task.notes,
importance: task.importance, importance: task.importance,
difficulty: task.difficulty,
challenge: task.challenge, challenge: task.challenge,
fromSystem: task.fromSystem, fromSystem: task.fromSystem,
creation_date: task.creation_date, creation_date: task.creation_date,
@ -95,7 +59,7 @@ function KanbanBoard() {
const columnsResponse = await axiosInstance.get("/lists"); const columnsResponse = await axiosInstance.get("/lists");
// Transform // Transform
const transformedColumns = columnsResponse.data.map(column => ({ const transformedColumns = columnsResponse.data.map((column) => ({
id: column.id, id: column.id,
title: column.name, title: column.name,
})); }));
@ -135,7 +99,7 @@ function KanbanBoard() {
<div className="ml-2 flex gap-4"> <div className="ml-2 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) => (
<ColumnContainerCard <ColumnContainerCard
key={col.id} key={col.id}
column={col} column={col}
@ -144,36 +108,11 @@ 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>
</div> </div>
{/* create new column */}
<button
onClick={() => {
createNewColumn();
}}
className="
h-[60px]
w-[268px]
max-w-[268px]
cursor-pointer
rounded-xl
bg-[#f1f2f4]
border-2
p-4
hover:bg-gray-200
flex
gap-2
my-2
bg-opacity-60
">
<div className="my-1">
<AiOutlinePlusCircle />
</div>
Add Column
</button>
</div> </div>
{createPortal( {createPortal(
@ -186,7 +125,7 @@ function KanbanBoard() {
createTask={createTask} createTask={createTask}
deleteTask={deleteTask} deleteTask={deleteTask}
updateTask={updateTask} updateTask={updateTask}
tasks={tasks.filter(task => task.columnId === activeColumn.id)} tasks={tasks.filter((task) => task.columnId === activeColumn.id)}
/> />
)} )}
{activeTask && <TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />} {activeTask && <TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />}
@ -213,35 +152,34 @@ function KanbanBoard() {
axiosInstance axiosInstance
.post("todo/", newTaskData) .post("todo/", newTaskData)
.then(response => { .then((response) => {
const newTask = { const newTask = {
id: response.data.id, id: response.data.id,
columnId, columnId,
content: response.data.title, content: response.data.title,
}; };
}) })
.catch(error => { .catch((error) => {
console.error("Error creating task:", error); console.error("Error creating task:", error);
}); });
setTasks(tasks => [...tasks, newTask]); setTasks((tasks) => [...tasks, newTask]);
} }
function deleteTask(id) { function deleteTask(id) {
const newTasks = tasks.filter(task => task.id !== id); const newTasks = tasks.filter((task) => task.id !== id);
axiosInstance axiosInstance
.delete(`todo/${id}/`) .delete(`todo/${id}/`)
.then(response => { .then((response) => {
setTasks(newTasks); setTasks(newTasks);
}) })
.catch(error => { .catch((error) => {
console.error("Error deleting Task:", error); console.error("Error deleting Task:", error);
}); });
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 };
}); });
@ -252,15 +190,15 @@ function KanbanBoard() {
function createNewColumn() { function createNewColumn() {
axiosInstance axiosInstance
.post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id }) .post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id })
.then(response => { .then((response) => {
const newColumn = { const newColumn = {
id: response.data.id, id: response.data.id,
title: response.data.name, title: response.data.name,
}; };
setColumns(prevColumns => [...prevColumns, newColumn]); setColumns((prevColumns) => [...prevColumns, newColumn]);
}) })
.catch(error => { .catch((error) => {
console.error("Error creating ListBoard:", error); console.error("Error creating ListBoard:", error);
}); });
} }
@ -268,22 +206,22 @@ function KanbanBoard() {
function deleteColumn(id) { function deleteColumn(id) {
axiosInstance axiosInstance
.delete(`lists/${id}/`) .delete(`lists/${id}/`)
.then(response => { .then((response) => {
setColumns(prevColumns => prevColumns.filter(col => col.id !== id)); setColumns((prevColumns) => prevColumns.filter((col) => col.id !== id));
}) })
.catch(error => { .catch((error) => {
console.error("Error deleting ListBoard:", error); console.error("Error deleting ListBoard:", error);
}); });
const tasksToDelete = tasks.filter(t => t.columnId === id); const tasksToDelete = tasks.filter((t) => t.columnId === id);
tasksToDelete.forEach(task => { tasksToDelete.forEach((task) => {
axiosInstance axiosInstance
.delete(`todo/${task.id}/`) .delete(`todo/${task.id}/`)
.then(response => { .then((response) => {
setTasks(prevTasks => prevTasks.filter(t => t.id !== task.id)); setTasks((prevTasks) => prevTasks.filter((t) => t.id !== task.id));
}) })
.catch(error => { .catch((error) => {
console.error("Error deleting Task:", error); console.error("Error deleting Task:", error);
}); });
}); });
@ -293,10 +231,10 @@ function KanbanBoard() {
// Update the column // Update the column
axiosInstance axiosInstance
.patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements .patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements
.then(response => { .then((response) => {
setColumns(prevColumns => prevColumns.map(col => (col.id === id ? { ...col, title } : col))); setColumns((prevColumns) => prevColumns.map((col) => (col.id === id ? { ...col, title } : col)));
}) })
.catch(error => { .catch((error) => {
console.error("Error updating ListBoard:", error); console.error("Error updating ListBoard:", error);
}); });
} }
@ -330,9 +268,9 @@ function KanbanBoard() {
// Reorder columns if the dragged item is a column // Reorder columns if the dragged item is a column
if (isActiveAColumn && isOverAColumn) { if (isActiveAColumn && isOverAColumn) {
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);
const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex); const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex);
@ -342,9 +280,9 @@ function KanbanBoard() {
// Reorder tasks within the same column // Reorder tasks within the same column
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);
const reorderedTasks = arrayMove(tasks, activeIndex, overIndex); const reorderedTasks = arrayMove(tasks, activeIndex, overIndex);
@ -354,15 +292,15 @@ function KanbanBoard() {
// Move tasks between columns and update columnId // Move tasks between columns and update columnId
if (isActiveATask && isOverAColumn) { if (isActiveATask && isOverAColumn) {
setTasks(tasks => { setTasks((tasks) => {
const activeIndex = tasks.findIndex(t => t.id === activeId); const activeIndex = tasks.findIndex((t) => t.id === activeId);
tasks[activeIndex].columnId = overId; tasks[activeIndex].columnId = overId;
axiosInstance axiosInstance
.put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 }) .put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 })
.then(response => {}) .then((response) => {})
.catch(error => { .catch((error) => {
console.error("Error updating task columnId:", error); console.error("Error updating task columnId:", error);
}); });
@ -386,9 +324,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;
@ -402,18 +340,12 @@ 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);
}); });
} }
} }
function generateId() {
return Math.floor(Math.random() * 10001);
}
} }
export default KanbanBoard;

View File

@ -1,12 +1,12 @@
import KanbanBoard from "./kanbanBoard"; import { KanbanBoard } from "./kanbanBoard";
import React, { useState } from 'react'; import { useState } from "react";
const KanbanPage = () => { export const KanbanPage = () => {
const [activeTab, setActiveTab] = useState('kanban'); const [activeTab, setActiveTab] = useState("kanban");
const handleTabClick = (tabId) => { const handleTabClick = (tabId) => {
setActiveTab(tabId); setActiveTab(tabId);
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
@ -29,10 +29,7 @@ const KanbanPage = () => {
</div> </div>
</div> </div>
<KanbanBoard /> <KanbanBoard />
<div className="flex justify-center border-2 "> <div className="flex justify-center border-2 "></div>
</div>
</div> </div>
); );
}; };
export default KanbanPage;

View File

@ -2,9 +2,9 @@ import { useState } from "react";
import { BsFillTrashFill } from "react-icons/bs"; import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import TaskDetailModal from "./taskDetailModal"; import { TaskDetailModal } from "./taskDetailModal";
function TaskCard({ task, deleteTask, updateTask}) { export function TaskCard({ task, deleteTask, updateTask }) {
const [mouseIsOver, setMouseIsOver] = useState(false); const [mouseIsOver, setMouseIsOver] = useState(false);
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
@ -15,7 +15,6 @@ function TaskCard({ task, deleteTask, updateTask}) {
}, },
}); });
const style = { const style = {
transition, transition,
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@ -45,7 +44,7 @@ function TaskCard({ task, deleteTask, updateTask}) {
description={task.description} description={task.description}
tags={task.tags} tags={task.tags}
difficulty={task.difficulty} difficulty={task.difficulty}
challenge={task.challenge} f challenge={task.challenge}
importance={task.importance} importance={task.importance}
/> />
<div <div
@ -79,5 +78,3 @@ function TaskCard({ task, deleteTask, updateTask}) {
</div> </div>
); );
} }
export default TaskCard;

View File

@ -1,9 +1,9 @@
import React, { useState } from "react"; import { useState } from "react";
import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6"; import { FaPlus } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb"; import { TbChecklist } from "react-icons/tb";
function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) { export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) {
const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance); const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
@ -28,7 +28,8 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo
<div className="flex flex-col"> <div className="flex flex-col">
<h3 className="font-bold text-lg"> <h3 className="font-bold text-lg">
<span className="flex gap-2"> <span className="flex gap-2">
{<FaTasks className="my-2" />}{title} {<FaTasks className="my-2" />}
{title}
</span> </span>
</h3> </h3>
<p className="text-xs">{title}</p> <p className="text-xs">{title}</p>
@ -45,13 +46,13 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> <ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li> <li>
<a> <a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm"/> <input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2 Item 2
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="flex flex-nowrap overflow-x-auto"></div> <div className="flex flex-nowrap overflow-x-auto"></div>
</div> </div>
@ -144,5 +145,3 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo
</dialog> </dialog>
); );
} }
export default TaskDetailModal;

View File

@ -0,0 +1,44 @@
import { FloatingParticles } from "../FlaotingParticles";
export function LandingPage() {
return (
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
{/* Particles Container */}
<FloatingParticles />
{/* Navbar */}
<div className="relative" id="home">
<div className="max-w-7xl mx-auto px-6 md:px-12 xl:px-6">
<div className="relative pt-36 ml-auto">
<div className="lg:w-2/3 text-center mx-auto">
<h1 className="text-#143D6C font-bold text-5xl md:text-6xl xl:text-7xl">
Manage your task with{" "}
<span className="text-primary">
TurTask
<label className="swap swap-flip text-6xl">
<input type="checkbox" />
<div className="swap-on">😇</div>
<div className="swap-off">🥳</div>
</label>
</span>
</h1>
<p className="mt-8 text-#143D6C">
Unleash productivity with our personal task and project
management.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-y-4 gap-x-6">
<a
href="/login"
className="relative flex h-11 w-full items-center justify-center px-6 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max"
>
<span className="relative text-base font-semibold text-white">
Get started
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,9 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList, AiOutlinePieChart } from "react-icons/ai"; import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList } from "react-icons/ai";
import { PiStepsDuotone } from "react-icons/pi"; import { PiStepsDuotone } from "react-icons/pi";
import { IoSettingsOutline } from "react-icons/io5"; import { IoSettingsOutline } from "react-icons/io5";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { Link, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
const menuItems = [ const menuItems = [
{ id: 0, path: "/", icon: <AiOutlineHome /> }, { id: 0, path: "/", icon: <AiOutlineHome /> },
@ -13,20 +13,12 @@ const menuItems = [
{ id: 4, path: "/priority", icon: <PiStepsDuotone /> }, { id: 4, path: "/priority", icon: <PiStepsDuotone /> },
]; ];
const IconSideNav = () => { export const SideNav = () => {
return (
<div className="bg-slate-900 text-slate-100 flex">
<SideNav />
</div>
);
};
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-full fixed top-0 left-0 z-50"> <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}
icon={item.icon} icon={item.icon}
@ -65,5 +57,3 @@ const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
</motion.button> </motion.button>
); );
}; };
export default IconSideNav;

View File

@ -0,0 +1,16 @@
import { Link } from "react-router-dom";
export const NavPreLogin = ({ text, btn_text, link }) => {
return (
<div className="navbar bg-base-100 sticky top-0 z-50 border-2 border-neutral-400">
<div className="navbar-start"></div>
<div className="navbar-center hidden lg:flex"></div>
<div className="navbar-end space-x-3">
<p className="font-bold">{text}</p>
<Link to={link} className="btn bg-blue-700 hover:bg-blue-900 text-white font-bold">
{btn_text}
</Link>
</div>
</div>
);
};

View File

@ -1,20 +1,18 @@
import * as React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import axiosapi from "../../api/AuthenticationApi"; import { apiUserLogout } from "src/api/AuthenticationApi";
import { useAuth } from "../../hooks/authentication/IsAuthenticated"; import { useAuth } from "src/hooks/AuthHooks";
const settings = { const settings = {
Profile: '/profile', Profile: "/profile",
Account: '/account', Account: "/account",
}; };
function NavBar() { export function NavBar() {
const Navigate = useNavigate(); const Navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth(); const { isAuthenticated, setIsAuthenticated } = useAuth();
const logout = () => { const logout = () => {
axiosapi.apiUserLogout(); apiUserLogout();
setIsAuthenticated(false); setIsAuthenticated(false);
Navigate("/"); Navigate("/");
}; };
@ -67,4 +65,3 @@ function NavBar() {
</div> </div>
); );
} }
export default NavBar;

View File

@ -1,7 +1,7 @@
import React, { useState, useRef } from "react"; import { useState, useRef } from "react";
import { ApiUpdateUserProfile } from "../api/UserProfileApi"; import { ApiUpdateUserProfile } from "src/api/UserProfileApi";
function ProfileUpdateComponent() { export function ProfileUpdateComponent() {
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [fullName, setFullName] = useState(""); const [fullName, setFullName] = useState("");
@ -15,7 +15,7 @@ function ProfileUpdateComponent() {
} }
}; };
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);
@ -66,7 +66,7 @@ function ProfileUpdateComponent() {
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>
@ -78,7 +78,7 @@ function ProfileUpdateComponent() {
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>
@ -89,7 +89,7 @@ function ProfileUpdateComponent() {
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>
@ -100,5 +100,3 @@ function ProfileUpdateComponent() {
</div> </div>
); );
} }
export default ProfileUpdateComponent;

View File

@ -1,7 +1,6 @@
import * as React from "react"; import { ProfileUpdateComponent } from "./ProfileUpdateComponent";
import ProfileUpdateComponent from "./ProfileUpdateComponent";
function ProfileUpdatePage() { export function ProfileUpdatePage() {
return ( return (
<div> <div>
<div className="stats shadow mt-3"> <div className="stats shadow mt-3">
@ -47,7 +46,7 @@ function ProfileUpdatePage() {
</div> </div>
</div> </div>
<div className="stat-desc py-2">3213/321312321 points</div> <div className="stat-desc py-2">3213/321312321 points</div>
<progress class="progress progress-info w-36" value="10" max="100"></progress> <progress className="progress progress-info w-36" value="10" max="100"></progress>
</div> </div>
<div className="stat"> <div className="stat">
@ -75,10 +74,10 @@ function ProfileUpdatePage() {
</div> </div>
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div class="card-body"> <div className="card-body">
<h2 class="card-title">About me</h2> <h2 className="card-title">About me</h2>
<div class="card-actions justify-end"></div> <div className="card-actions justify-end"></div>
<textarea class="textarea textarea-bordered textarea-lg w-full" disabled> <textarea className="textarea textarea-bordered textarea-lg w-full" disabled>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum dolores recusandae, officiis consequuntur 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 nam, non ab commodi totam mollitia iusto nemo voluptatum error aliquam similique perspiciatis, eligendi
nulla. Animi, sit? nulla. Animi, sit?
@ -89,31 +88,31 @@ function ProfileUpdatePage() {
<div className="grid grid-cols-2 grid-rows-2 gap-4 my-2"> <div className="grid grid-cols-2 grid-rows-2 gap-4 my-2">
<div className="col-span-full"> <div className="col-span-full">
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div class="card-body"> <div className="card-body">
<h2 class="card-title">Overall Statistics</h2> <h2 className="card-title">Overall Statistics</h2>
<div class="card-actions justify-end"></div> <div className="card-actions justify-end"></div>
</div> </div>
</div> </div>
</div> </div>
<div className="col-start-2 row-start-2"> <div className="col-start-2 row-start-2">
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div class="card-body"> <div className="card-body">
<h2 class="card-title">Achievements</h2> <h2 className="card-title">Achievements</h2>
<div class="card-actions justify-end"></div> <div className="card-actions justify-end"></div>
</div> </div>
</div> </div>
</div> </div>
<div className="col-start-1 row-start-2"> <div className="col-start-1 row-start-2">
<div className="card bg-base-100 shadow"> <div className="card bg-base-100 shadow">
<div class="card-body"> <div className="card-body">
<h2 class="card-title">Friends</h2> <h2 className="card-title">Friends</h2>
<div class="card-actions justify-end"></div> <div className="card-actions justify-end"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="fixed bottom-4 right-4"> <div className="fixed bottom-4 right-4">
<ul className="menu menu-horizontal bg-base-200 rounded-box"> <ul className="menu menu-horizontal bg-base-200 rounded-box">
<li> <li>
<a onClick={() => document.getElementById("my_modal_4").showModal()}> <a onClick={() => document.getElementById("my_modal_4").showModal()}>
@ -136,11 +135,10 @@ function ProfileUpdatePage() {
<div className="modal-box w-11/12 max-w-5xl flex flex-col"> <div className="modal-box w-11/12 max-w-5xl flex flex-col">
<form method="dialog"> <form method="dialog">
<ProfileUpdateComponent /> <ProfileUpdateComponent />
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form> </form>
</div> </div>
</dialog> </dialog>
</div> </div>
); );
} }
export default ProfileUpdatePage;

View File

@ -1,115 +0,0 @@
import React, { useState } from "react";
import axiosapi from "../api/axiosapi";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import CssBaseline from "@material-ui/core/CssBaseline";
import Container from "@material-ui/core/Container";
import Button from "@material-ui/core/Button";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
// Styles for various elements
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%",
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const Signup = () => {
const classes = useStyles();
const [formData, setFormData] = useState({
email: "",
username: "",
password: "",
});
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async e => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
axiosapi.createUser(formData);
} catch (error) {
console.error("Error creating user:", error);
setError("Registration failed. Please try again."); // Set an error message
} finally {
setIsSubmitting(false);
}
};
const handleChange = e => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Sign Up
</Typography>
<form className={classes.form} onSubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
type="email"
name="email"
fullWidth
value={formData.email}
onChange={handleChange}
label="Email"
/>
<TextField
variant="outlined"
margin="normal"
type="text"
name="username"
fullWidth
value={formData.username}
onChange={handleChange}
label="Username"
/>
<TextField
variant="outlined"
margin="normal"
type="password"
name="password"
fullWidth
value={formData.password}
onChange={handleChange}
label="Password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={isSubmitting}>
{isSubmitting ? "Signing up..." : "Sign Up"}
</Button>
</form>
{error && <Typography color="error">{error}</Typography>}
</div>
</Container>
);
};
export default Signup;

View File

@ -1,39 +1,36 @@
import React from 'react'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faGithub } from "@fortawesome/free-brands-svg-icons";
import { faGoogle, faGithub } from '@fortawesome/free-brands-svg-icons';
function Signup() { export function Signup() {
return ( return (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
<div className="flex flex-col items-center bg-white p-10 rounded-lg shadow-md"> <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> <h1 className="text-4xl font-semibold mb-6">Create your account</h1>
<p className="text-gray-700 mb-6 text-lg"> <p className="text-gray-700 mb-6 text-lg">Start spending more time on your own table.</p>
Start spending more time on your own table. <div className="font-bold">
</p> <div className="mb-4">
<div className='font-bold'> <button className="flex items-center justify-center bg-blue-500 text-white px-14 py-3 rounded-lg">
<div className="mb-4"> <span className="mr-3">
<button className="flex items-center justify-center bg-blue-500 text-white px-14 py-3 rounded-lg"> <FontAwesomeIcon icon={faGoogle} />
<span className="mr-3"><FontAwesomeIcon icon={faGoogle} /></span> </span>
Sign Up with Google Sign Up with Google
</button> </button>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<button className="flex items-center justify-center bg-gray-800 text-white px-14 py-3 rounded-lg"> <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> <span className="mr-3">
Sign Up with Github <FontAwesomeIcon icon={faGithub} />
</button> </span>
</div> Sign Up with Github
</button>
</div>
<div> <div>
<button className="bg-green-500 text-white px-14 py-3 rounded-lg"> <button className="bg-green-500 text-white px-14 py-3 rounded-lg">Sign Up with your email.</button>
Sign Up with your email. </div>
</button>
</div>
</div>
</div>
</div> </div>
); </div>
</div>
);
} }
export default Signup;

View File

@ -1,47 +0,0 @@
import React, { useState, useEffect } from "react";
import axiosapi from "../api/AuthenticationApi";
import { Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
function TestAuth() {
let Navigate = useNavigate();
const [message, setMessage] = useState("");
useEffect(() => {
// Fetch the "hello" data from the server when the component mounts
axiosapi
.getGreeting()
.then(res => {
console.log(res.data);
setMessage(res.data.user);
})
.catch(err => {
console.log(err);
setMessage("");
});
}, []);
const logout = () => {
// Log out the user, clear tokens, and navigate to the "/testAuth" route
axiosapi.apiUserLogout();
Navigate("/testAuth");
};
return (
<div>
{message !== "" && (
<div>
<h1 class="text-xl font-bold">Login! Hello!</h1>
<h2>{message}</h2>
<Button variant="contained" onClick={logout}>
Logout
</Button>
</div>
)}
{message === "" && <h1 class="text-xl font-bold">Need to sign in, No authentication found</h1>}
</div>
);
}
export default TestAuth;

View File

@ -0,0 +1,29 @@
import { useEffect } from "react";
import PropTypes from "prop-types";
import { createContext, useState } from "react";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const accessToken = localStorage.getItem("access_token");
if (accessToken) {
setIsAuthenticated(true);
}
}, []);
const contextValue = {
isAuthenticated,
setIsAuthenticated,
};
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthContext;

View File

@ -0,0 +1,36 @@
import { useContext } from "react";
import AuthContext from "src/contexts/AuthContextProvider";
/**
* useAuth - Custom React Hook for Accessing Authentication Context
*
* @returns {Object} An object containing:
* - {boolean} isAuthenticated: A boolean indicating whether the user is authenticated.
* - {function} setIsAuthenticated: A function to set the authentication status manually.
*
* @throws {Error} If used outside the context of an AuthProvider.
*
* @example
* // Import the hook
* import useAuth from './AuthHooks';
*
* // Inside a functional component
* const { isAuthenticated, setIsAuthenticated } = useAuth();
*
* // Check authentication status
* if (isAuthenticated) {
* // User is authenticated
* } else {
* // User is not authenticated
* }
*
* // Manually set authentication status
* setIsAuthenticated(true);
*/
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@ -1,39 +0,0 @@
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

@ -1,9 +1,9 @@
import React, { Fragment } from "react"; import { 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"; import { AuthProvider } from "./contexts/AuthContextProvider";
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
@ -12,7 +12,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter> <BrowserRouter>
<Fragment> <Fragment>
<AuthProvider> <AuthProvider>
<App /> <App />
</AuthProvider> </AuthProvider>
</Fragment> </Fragment>
</BrowserRouter> </BrowserRouter>

View File

@ -3,17 +3,14 @@
const defaultTheme = require("tailwindcss/defaultTheme"); const defaultTheme = require("tailwindcss/defaultTheme");
export default { export default {
content: [ content: ["./src/**/*.{js,jsx}", "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
"./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:{ colors: {
tremor: { tremor: {
brand: { brand: {
faint: "#eff6ff", // blue-50 faint: "#eff6ff", // blue-50
@ -42,10 +39,9 @@ export default {
strong: "#111827", // gray-900 strong: "#111827", // gray-900
inverted: "#ffffff", // white inverted: "#ffffff", // white
}, },
}, },
}, },
boxShadow:{ boxShadow: {
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", "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-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)", "tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
@ -92,12 +88,7 @@ export default {
/^(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))$/, /^(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: [ plugins: [require("daisyui"), require("@tailwindcss/typography"), require("@headlessui/tailwindcss")],
require("daisyui"),
require("@tailwindcss/typography"),
require("daisyui"),
require("@headlessui/tailwindcss"),
],
daisyui: { daisyui: {
themes: ["light", "night"], themes: ["light", "night"],
}, },

View File

@ -1,7 +1,14 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) resolve: {
alias: {
src: "/src",
},
},
define: {
__APP_ENV__: process.env.VITE_VERCEL_ENV,
},
});