diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 1896a3e..0cc3f29 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -2,13 +2,12 @@ name: Django CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: build: - runs-on: ubuntu-latest services: @@ -24,20 +23,21 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.11 - uses: actions/setup-python@v2 - with: - python-version: 3.11 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run migrations - run: | - cd backend - python manage.py migrate - - name: Run tests - run: | - cd backend - python manage.py test + - uses: actions/checkout@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - name: Install dependencies + run: | + cd backend + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run migrations + run: | + cd backend + python manage.py migrate + - name: Run tests + run: | + cd backend + python manage.py test diff --git a/.gitignore b/.gitignore index d6a7e2b..a40fc56 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ cover/ # Django stuff: *.log -local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/backend/authentications/urls.py b/backend/authentications/urls.py index 32965a4..6029eea 100644 --- a/backend/authentications/urls.py +++ b/backend/authentications/urls.py @@ -1,12 +1,11 @@ from django.urls import path 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 = [ path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), 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/status/', CheckAccessTokenAndRefreshToken.as_view(), name='check_token_status') ] \ No newline at end of file diff --git a/backend/authentications/views.py b/backend/authentications/views.py index 9b5f249..87f662e 100644 --- a/backend/authentications/views.py +++ b/backend/authentications/views.py @@ -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.""" import json @@ -10,14 +7,11 @@ from django.conf import settings from django.contrib.auth.hashers import make_password 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.views import APIView from rest_framework_simplejwt.tokens import RefreshToken - -from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter - -from dj_rest_auth.registration.views import SocialLoginView +from rest_framework_simplejwt.authentication import JWTAuthentication from google_auth_oauthlib.flow import InstalledAppFlow @@ -27,6 +21,31 @@ from users.managers import CustomAccountManager 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): """ Custom Token Obtain Pair View. @@ -45,39 +64,6 @@ class ObtainTokenPairWithCustomView(APIView): 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): """ 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) if response.status_code == 200: return response.json() - raise Exception('Google API Error', response) + raise Exception('Google API Error', response) \ No newline at end of file diff --git a/backend/core/local_settings.py b/backend/core/local_settings.py new file mode 100644 index 0000000..9e7903c --- /dev/null +++ b/backend/core/local_settings.py @@ -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", + }, +} \ No newline at end of file diff --git a/backend/core/production_settings.py b/backend/core/production_settings.py new file mode 100644 index 0000000..dd9eeb3 --- /dev/null +++ b/backend/core/production_settings.py @@ -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", + }, +} \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index 9e7903c..7532f01 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -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 -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", - }, -} \ No newline at end of file +if os.environ.get("DJANGO_ENV") == "PRODUCTION": + from .production_settings import * +else: + from .local_settings import * \ No newline at end of file diff --git a/backend/core/wsgi.py b/backend/core/wsgi.py index f44964d..a08c895 100644 --- a/backend/core/wsgi.py +++ b/backend/core/wsgi.py @@ -11,6 +11,6 @@ import os 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() diff --git a/backend/railway.json b/backend/railway.json new file mode 100644 index 0000000..a99f1a8 --- /dev/null +++ b/backend/railway.json @@ -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 + } +} diff --git a/requirements.txt b/backend/requirements.txt similarity index 87% rename from requirements.txt rename to backend/requirements.txt index 344b980..95962f8 100644 --- a/requirements.txt +++ b/backend/requirements.txt @@ -15,4 +15,6 @@ google-auth-httplib2>=0.1 django-storages[s3]>=1.14 Pillow>=10.1 drf-spectacular>=0.26 -python-dateutil>=2.8 \ No newline at end of file +python-dateutil>=2.8 +gunicorn==21.2.0 +packaging==23.1 \ No newline at end of file diff --git a/backend/users/migrations/0005_alter_userstats_endurance_and_more.py b/backend/users/migrations/0005_alter_userstats_endurance_and_more.py index 35fe6f7..76ec5c3 100644 --- a/backend/users/migrations/0005_alter_userstats_endurance_and_more.py +++ b/backend/users/migrations/0005_alter_userstats_endurance_and_more.py @@ -25,7 +25,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='userstats', name='luck', - field=models.IntegerField(default=users.models.random_luck, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]), + field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]), ), migrations.AlterField( model_name='userstats', diff --git a/backend/users/models.py b/backend/users/models.py index 7a765a6..2fe1df6 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -31,14 +31,12 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # Fields for authentication USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['username', 'first_name'] + REQUIRED_FIELDS = [] def __str__(self): # String representation of the user return self.username - -def random_luck(): - return random.randint(1, 50) + class UserStats(models.Model): """ diff --git a/backend/users/views.py b/backend/users/views.py index af201b7..9d022e3 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -7,6 +7,8 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser +from rest_framework_simplejwt.tokens import RefreshToken + from users.serializers import CustomUserSerializer, UpdateProfileSerializer from users.models import CustomUser @@ -25,7 +27,9 @@ class CustomUserCreate(APIView): if serializer.is_valid(): user = serializer.save() 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) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 4dcb439..809eec3 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,20 +1,18 @@ module.exports = { root: true, - env: { browser: true, es2020: true }, + env: { browser: true, es2020: true, node: true }, extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, + settings: { react: { version: "18.2" } }, + plugins: ["react-refresh"], rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "react/prop-types": 0, }, -} +}; diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..af4aef6 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "src/*": ["./src/*"] + } + } +} diff --git a/frontend/package.json b/frontend/package.json index e5ae2dd..f057dce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", "jwt-decode": "^4.0.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e4ccb3d..22fdadd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -89,6 +89,9 @@ dependencies: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + prop-types: + specifier: ^15.8.1 + version: 15.8.1 react: specifier: ^18.2.0 version: 18.2.0 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b6d42b2..a917ca5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,36 +1,91 @@ import "./App.css"; -import { Route, Routes, useLocation } from "react-router-dom"; - -import TestAuth from "./components/testAuth"; -import LoginPage from "./components/authentication/LoginPage"; -import SignUpPage from "./components/authentication/SignUpPage"; -import NavBar from "./components/navigations/Navbar"; -import Calendar from "./components/calendar/calendar"; -import KanbanPage from "./components/kanbanBoard/kanbanPage"; -import IconSideNav from "./components/navigations/IconSideNav"; -import Eisenhower from "./components/EisenhowerMatrix/Eisenhower"; -import PrivateRoute from "./PrivateRoute"; -import ProfileUpdatePage from "./components/profilePage"; -import Dashboard from "./components/dashboard/dashboard"; +import axios from "axios"; +import { useEffect } from "react"; +import { Route, Routes, Navigate } from "react-router-dom"; +import { LoginPage } from "./components/authentication/LoginPage"; +import { SignUp } from "./components/authentication/SignUpPage"; +import { NavBar } from "./components/navigations/Navbar"; +import { Calendar } from "./components/calendar/calendar"; +import { KanbanPage } from "./components/kanbanBoard/kanbanPage"; +import { SideNav } from "./components/navigations/IconSideNav"; +import { Eisenhower } from "./components/EisenhowerMatrix/Eisenhower"; +import { PrivateRoute } from "./PrivateRoute"; +import { ProfileUpdatePage } from "./components/profile/profilePage"; +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 location = useLocation(); - const prevention = ["/login", "/signup"]; - const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_)); + const { isAuthenticated, setIsAuthenticated } = useAuth(); + 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
{isAuthenticated ? : }
; +}; + +const NonAuthenticatedComponents = () => { return ( -
- {!isLoginPageOrSignUpPage && } -
+
+ + }> + } /> + + }> + } /> + + }> + } /> + + } /> + +
+ ); +}; + +const AuthenticatedComponents = () => { + return ( +
+ +
-
+
} /> }> } /> - } /> }> } /> @@ -40,8 +95,7 @@ const App = () => { }> } /> - } /> - } /> + } />
diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx index defeaea..3daa62b 100644 --- a/frontend/src/PrivateRoute.jsx +++ b/frontend/src/PrivateRoute.jsx @@ -1,11 +1,7 @@ -import React from "react"; import { Navigate, Outlet } from "react-router-dom"; -import { useAuth } from "./hooks/authentication/IsAuthenticated"; +import { useAuth } from "src/hooks/AuthHooks"; -const PrivateRoute = () => { - const { isAuthenticated, setIsAuthenticated } = useAuth(); - const auth = isAuthenticated; - return auth ? : ; +export const PrivateRoute = () => { + const { isAuthenticated } = useAuth(); + return isAuthenticated ? : ; }; - -export default PrivateRoute; \ No newline at end of file diff --git a/frontend/src/PublicRoute.jsx b/frontend/src/PublicRoute.jsx new file mode 100644 index 0000000..69f980a --- /dev/null +++ b/frontend/src/PublicRoute.jsx @@ -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 ? : ; +}; diff --git a/frontend/src/api/AuthenticationApi.jsx b/frontend/src/api/AuthenticationApi.jsx index d4b6486..e029404 100644 --- a/frontend/src/api/AuthenticationApi.jsx +++ b/frontend/src/api/AuthenticationApi.jsx @@ -1,72 +1,43 @@ 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 -const apiUserLogin = data => { +export const apiUserLogin = (data) => { return axiosInstance .post("token/obtain/", data) - .then(response => { - console.log(response.statusText); - return response; - }) - .catch(error => { - console.log("apiUserLogin error: ", error); - return error; + .then((response) => response) + .catch((error) => { + throw error; }); }; // Function for user logout -const apiUserLogout = () => { +export const apiUserLogout = () => { axiosInstance.defaults.headers["Authorization"] = ""; // Clear authorization header localStorage.removeItem("access_token"); // Remove access token localStorage.removeItem("refresh_token"); // Remove refresh token }; // Function for Google login -const googleLogin = async token => { +export const googleLogin = async (token) => { 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, }); // console.log('service google login res: ', 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 -const createUser = async formData => { +export const createUser = async (formData) => { try { axios.defaults.withCredentials = true; - const resposne = axios.post("http://localhost:8000/api/user/create/", formData); - // const response = await axiosInstance.post('/user/create/', formData); + const response = await axios.post(`${baseURL}user/create/`, formData); return response.data; - } catch (error) { - throw error; + } catch (e) { + console.error("Error in createUser function:", e); + throw e; } }; - -// Export the functions and Axios instance -export default { - apiUserLogin, - apiUserLogout, - getGreeting: getGreeting, - googleLogin, - createUser, -}; diff --git a/frontend/src/api/AxiosConfig.jsx b/frontend/src/api/AxiosConfig.jsx new file mode 100644 index 0000000..29ed6c7 --- /dev/null +++ b/frontend/src/api/AxiosConfig.jsx @@ -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); + } +); diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx index eee70d0..098d934 100644 --- a/frontend/src/api/TaskApi.jsx +++ b/frontend/src/api/TaskApi.jsx @@ -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) => { return axiosInstance .post(`${baseURL}${endpoint}/`, data) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; -export const readTasks = endpoint => { +export const readTasks = (endpoint) => { return axiosInstance .get(`${baseURL}${endpoint}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -23,8 +23,8 @@ export const readTasks = endpoint => { export const readTaskByID = (endpoint, id) => { return axiosInstance .get(`${baseURL}${endpoint}/${id}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -32,8 +32,8 @@ export const readTaskByID = (endpoint, id) => { export const updateTask = (endpoint, id, data) => { return axiosInstance .put(`${baseURL}${endpoint}/${id}/`, data) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -41,16 +41,16 @@ export const updateTask = (endpoint, id, data) => { export const deleteTask = (endpoint, id) => { return axiosInstance .delete(`${baseURL}${endpoint}/${id}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; // Create -export const createTodoTask = data => createTask("todo", data); -export const createRecurrenceTask = data => createTask("daily", data); -export const createHabitTask = data => createTask("habit", data); +export const createTodoTask = (data) => createTask("todo", data); +export const createRecurrenceTask = (data) => createTask("daily", data); +export const createHabitTask = (data) => createTask("habit", data); // Read export const readTodoTasks = () => readTasks("todo"); @@ -58,9 +58,9 @@ export const readRecurrenceTasks = () => readTasks("daily"); export const readHabitTasks = () => readTasks("habit"); // Read by ID -export const readTodoTaskByID = id => readTaskByID("todo", id); -export const readRecurrenceTaskByID = id => readTaskByID("daily", id); -export const readHabitTaskByID = id => readTaskByID("habit", id); +export const readTodoTaskByID = (id) => readTaskByID("todo", id); +export const readRecurrenceTaskByID = (id) => readTaskByID("daily", id); +export const readHabitTaskByID = (id) => readTaskByID("habit", id); // Update export const updateTodoTask = (id, data) => updateTask("todo", id, data); @@ -68,6 +68,6 @@ export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data); // Delete -export const deleteTodoTask = id => deleteTask("todo", id); -export const deleteRecurrenceTask = id => deleteTask("daily", id); -export const deleteHabitTask = id => deleteTask("habit", id); +export const deleteTodoTask = (id) => deleteTask("todo", id); +export const deleteRecurrenceTask = (id) => deleteTask("daily", id); +export const deleteHabitTask = (id) => deleteTask("habit", id); diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx index e7a1b17..6dfc050 100644 --- a/frontend/src/api/UserProfileApi.jsx +++ b/frontend/src/api/UserProfileApi.jsx @@ -1,8 +1,10 @@ import axios from "axios"; -const ApiUpdateUserProfile = async formData => { +const baseURL = import.meta.env.VITE_BASE_URL; + +const ApiUpdateUserProfile = async (formData) => { 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: { Authorization: "Bearer " + localStorage.getItem("access_token"), "Content-Type": "multipart/form-data", diff --git a/frontend/src/api/configs/AxiosConfig.jsx b/frontend/src/api/configs/AxiosConfig.jsx deleted file mode 100644 index b0410d1..0000000 --- a/frontend/src/api/configs/AxiosConfig.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/assets/calendar.png b/frontend/src/assets/calendar.png deleted file mode 100644 index 8c30ce3..0000000 Binary files a/frontend/src/assets/calendar.png and /dev/null differ diff --git a/frontend/src/assets/home.png b/frontend/src/assets/home.png deleted file mode 100644 index c40c6b2..0000000 Binary files a/frontend/src/assets/home.png and /dev/null differ diff --git a/frontend/src/assets/pie-chart.png b/frontend/src/assets/pie-chart.png deleted file mode 100644 index 376d9e9..0000000 Binary files a/frontend/src/assets/pie-chart.png and /dev/null differ diff --git a/frontend/src/assets/planning.png b/frontend/src/assets/planning.png deleted file mode 100644 index 13a6bb2..0000000 Binary files a/frontend/src/assets/planning.png and /dev/null differ diff --git a/frontend/src/assets/plus.png b/frontend/src/assets/plus.png deleted file mode 100644 index a5252f5..0000000 Binary files a/frontend/src/assets/plus.png and /dev/null differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx index 374ac66..cc19aaa 100644 --- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx +++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx @@ -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 { readTodoTasks } from "../../api/TaskApi"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { axiosInstance } from "src/api/AxiosConfig"; function EachBlog({ name, colorCode, contentList, icon }) { const [tasks, setTasks] = useState(contentList); - const handleCheckboxChange = async index => { + const handleCheckboxChange = async (index) => { try { - setTasks(contentList) + setTasks(contentList); const updatedTasks = [...tasks]; const taskId = updatedTasks[index].id; @@ -55,17 +55,17 @@ function EachBlog({ name, colorCode, contentList, icon }) { ); } -function Eisenhower() { +export function Eisenhower() { const [tasks, setTasks] = useState([]); useEffect(() => { readTodoTasks() - .then(data => { + .then((data) => { console.log(data); - const contentList_ui = data.filter(task => task.priority === 1); - const contentList_uni = data.filter(task => task.priority === 2); - const contentList_nui = data.filter(task => task.priority === 3); - const contentList_nuni = data.filter(task => task.priority === 4); + const contentList_ui = data.filter((task) => task.priority === 1); + const contentList_uni = data.filter((task) => task.priority === 2); + const contentList_nui = data.filter((task) => task.priority === 3); + const contentList_nuni = data.filter((task) => task.priority === 4); setTasks({ contentList_ui, @@ -74,7 +74,7 @@ function Eisenhower() { contentList_nuni, }); }) - .catch(error => console.error("Error fetching tasks:", error)); + .catch((error) => console.error("Error fetching tasks:", error)); }, []); return ( @@ -108,5 +108,3 @@ function Eisenhower() {
); } - -export default Eisenhower; diff --git a/frontend/src/components/FlaotingParticles.jsx b/frontend/src/components/FlaotingParticles.jsx new file mode 100644 index 0000000..e621f55 --- /dev/null +++ b/frontend/src/components/FlaotingParticles.jsx @@ -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 ( +
+ +
+ ); +} diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx index 93f8f0b..28dd0a1 100644 --- a/frontend/src/components/authentication/LoginPage.jsx +++ b/frontend/src/components/authentication/LoginPage.jsx @@ -1,27 +1,19 @@ -import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { useNavigate, redirect } from "react-router-dom"; 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 { useAuth } from "src/hooks/AuthHooks"; +import { FloatingParticles } from "../FlaotingParticles"; +import { NavPreLogin } from "../navigations/NavPreLogin"; +import { apiUserLogin, googleLogin } from "src/api/AuthenticationApi"; - -function LoginPage() { +export function LoginPage() { + const { setIsAuthenticated } = useAuth(); const Navigate = useNavigate(); - const { isAuthenticated, setIsAuthenticated } = useAuth(); - - useEffect(() => { - if (!refreshAccessToken()) { - Navigate("/"); - } - }, []); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [error, setError] = useState(null); const handleEmailChange = (event) => { setEmail(event.target.value); @@ -30,28 +22,23 @@ function LoginPage() { const handlePasswordChange = (event) => { setPassword(event.target.value); }; + const handleSubmit = (event) => { event.preventDefault(); // Send a POST request to the authentication API - axiosapi - .apiUserLogin({ - email: email, - password: password, - }) + apiUserLogin({ + email: email, + password: password, + }) .then((res) => { - // On successful login, store tokens and set the authorization header localStorage.setItem("access_token", res.data.access); localStorage.setItem("refresh_token", res.data.refresh); - axiosapi.axiosInstance.defaults.headers["Authorization"] = - "Bearer " + res.data.access; setIsAuthenticated(true); - Navigate("/"); + redirect("/"); }) .catch((err) => { - console.log("Login failed"); - console.log(err); - setIsAuthenticated(false); + setError("Incorrect username or password"); }); }; @@ -60,7 +47,7 @@ function LoginPage() { redirect_uri: "postmessage", onSuccess: async (response) => { try { - const loginResponse = await axiosapi.googleLogin(response.code); + const loginResponse = await googleLogin(response.code); if (loginResponse && loginResponse.data) { const { access_token, refresh_token } = loginResponse.data; @@ -71,157 +58,96 @@ function LoginPage() { } } catch (error) { console.error("Error with the POST request:", error); - setIsAuthenticated(false); } }, 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 ( -
- {/* Particles Container */} -
- -
- {/* Login Box */} -
-
-

Login

- {/* Email Input */} -
- - -
- {/* Password Input */} -
- - -
- {/* Login Button */} - -
OR
- {/* Login with Google Button */} - - {/* Forgot Password Link */} -
- - Forgot your password? - +
+ +
+ {/* Particles Container */} + + + {/* Login Box */} +
+
+

Log in to your account

+ {/* Error Message */} + {error && ( +
+ + + + {error} +
+ )} + {/* Email Input */} +
+ + +
+ {/* Password Input */} +
+ + +
+ {/* Login Button */} + +
OR
+ {/* Login with Google Button */} +
); } - -export default LoginPage; diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx index 6bcc8dc..0492412 100644 --- a/frontend/src/components/authentication/SignUpPage.jsx +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -1,31 +1,15 @@ -import React, { useState } from "react"; +import { useState } from "react"; 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 { 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"; - -function Copyright(props) { - return ( -
- {"Copyright © "} - - TurTask - {" "} - {new Date().getFullYear()} - {"."} -
- ); -} - -export default function SignUp() { +export function SignUp() { const Navigate = useNavigate(); + const { setIsAuthenticated } = useAuth(); const [formData, setFormData] = useState({ email: "", @@ -40,47 +24,48 @@ export default function SignUp() { setIsSubmitting(true); setError(null); + const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + 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) { console.error("Error creating user:", error); setError("Registration failed. Please try again."); } finally { setIsSubmitting(false); } - Navigate("/login"); }; - const handleChange = (e) => { - const { name, value } = e.target; - setFormData({ ...formData, [name]: value }); - console.log(formData); + const handleEmailChange = (e) => { + setFormData({ ...formData, email: e.target.value }); }; - { - /* Particles Loader*/ - } - const particlesInit = useCallback(async (engine) => { - console.log(engine); - await loadFull(engine); - }, []); - const particlesLoaded = useCallback(async (container) => { - console.log(container); - }, []); + const handleUsernameChange = (e) => { + setFormData({ ...formData, username: e.target.value }); + }; + + const handlePasswordChange = (e) => { + setFormData({ ...formData, password: e.target.value }); + }; const googleLoginImplicit = useGoogleLogin({ flow: "auth-code", redirect_uri: "postmessage", onSuccess: async (response) => { try { - const loginResponse = await axiosapi.googleLogin(response.code); + const loginResponse = await googleLogin(response.code); if (loginResponse && loginResponse.data) { const { access_token, refresh_token } = loginResponse.data; localStorage.setItem("access_token", access_token); localStorage.setItem("refresh_token", refresh_token); setIsAuthenticated(true); - Navigate("/"); + Navigate("/profile"); } } catch (error) { console.error("Error with the POST request:", error); @@ -91,153 +76,83 @@ export default function SignUp() { }); return ( -
- {/* Particles Container */} -
- -
-
-
- {/* Register Form */} -

Signup

- {/* Email Input */} -
- - -
- {/* Username Input */} -
- - -
- {/* Password Input */} -
- - -
-

+
+ +
+ +
+
+ {/* Register Form */} +

Signup

+ {/* Email Input */} +
+ + +
+ {/* Username Input */} +
+ + +
+ {/* Password Input */} +
+ + +
+

- {/* Signups Button */} - -
OR
- {/* Login with Google Button */} - - {/* Already have an account? */} -
- - Already have an account? - + {/* Signups Button */} + +
OR
+ {/* Login with Google Button */} + + {/* Already have an account? */} +
-
diff --git a/frontend/src/components/authentication/refreshAcessToken.jsx b/frontend/src/components/authentication/refreshAcessToken.jsx deleted file mode 100644 index 11a74fa..0000000 --- a/frontend/src/components/authentication/refreshAcessToken.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/components/calendar/TaskDataHandler.jsx b/frontend/src/components/calendar/TaskDataHandler.jsx index ff8a5df..793ca4b 100644 --- a/frontend/src/components/calendar/TaskDataHandler.jsx +++ b/frontend/src/components/calendar/TaskDataHandler.jsx @@ -1,9 +1,9 @@ -import { readTodoTasks } from "../../api/TaskApi"; +import { readTodoTasks } from "src/api/TaskApi"; let eventGuid = 0; -const mapResponseToEvents = response => { - return response.map(item => ({ +const mapResponseToEvents = (response) => { + return response.map((item) => ({ id: item.id, title: item.title, start: item.start_event, diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index b4f8430..a85ea2d 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -1,13 +1,13 @@ -import React, { useState } from "react"; +import React from "react"; import { formatDate } from "@fullcalendar/core"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; 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 = { weekendsVisible: true, 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 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}'`)) { axiosInstance - .delete(`todo/${clickInfo.event.id}/`) - .then(response => { - clickInfo.event.remove(); - }) - .catch(error => { - console.error("Error deleting Task:", error); - }); + .delete(`todo/${clickInfo.event.id}/`) + .then((response) => { + clickInfo.event.remove(); + }) + .catch((error) => { + console.error("Error deleting Task:", error); + }); } }; - handleEvents = events => { + handleEvents = (events) => { this.setState({ currentEvents: events, }); diff --git a/frontend/src/components/dashboard/Areachart.jsx b/frontend/src/components/dashboard/Areachart.jsx index 8ede4d8..cd6bb93 100644 --- a/frontend/src/components/dashboard/Areachart.jsx +++ b/frontend/src/components/dashboard/Areachart.jsx @@ -1,103 +1,36 @@ import { AreaChart, Title } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { useState, useEffect } from "react"; +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 = () => { - const [value, setValue] = React.useState(null); - return ( - <> - Number of tasks statistics vs. last week - setValue(v)} - showAnimation - /> - - ); -}; \ No newline at end of file + const [areaChartDataArray, setAreaChartDataArray] = useState([]); + + useEffect(() => { + const fetchAreaChartData = async () => { + try { + const response = await axiosInstance.get("/dashboard/weekly/"); + const areaChartData = response.data; + setAreaChartDataArray(areaChartData); + } catch (error) { + console.error("Error fetching area chart data:", error); + } + }; + + fetchAreaChartData(); + }, []); + + return ( + <> + Number of tasks statistics vs. last week + + + ); +}; diff --git a/frontend/src/components/dashboard/Barchart.jsx b/frontend/src/components/dashboard/Barchart.jsx index 18c53fe..bf84a2b 100644 --- a/frontend/src/components/dashboard/Barchart.jsx +++ b/frontend/src/components/dashboard/Barchart.jsx @@ -1,89 +1,24 @@ import { BarChart, Title } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { useState, useEffect } from "react"; +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 = () => { - 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 ( <> Task completed statistics vs. last week @@ -94,7 +29,6 @@ export const BarChartGraph = () => { categories={["This Week", "Last Week"]} colors={["neutral", "indigo"]} yAxisWidth={30} - onValueChange={(v) => setValue(v)} showAnimation /> diff --git a/frontend/src/components/dashboard/DonutChart.jsx b/frontend/src/components/dashboard/DonutChart.jsx index c71b81d..63fc591 100644 --- a/frontend/src/components/dashboard/DonutChart.jsx +++ b/frontend/src/components/dashboard/DonutChart.jsx @@ -1,40 +1,37 @@ 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 () => { - try { - let res = await axiosInstance.get("/dashboard/stats/"); - // let todoCount = res.data.todo_count; - // let recurrenceCount = res.data.recurrence_count; - let todoCount = 10; - let recurrenceCount = 15; - if (todoCount === undefined) { - todoCount = 0; - } - if (recurrenceCount === undefined) { - recurrenceCount = 0; - } - const donutData = [ - { name: "Todo", count: todoCount }, - { name: "Recurrence", count: recurrenceCount }, - ]; - return donutData; - } catch (error) { - console.error("Error fetching donut data:", error); - return []; - } -}; +export function DonutChartGraph() { + const [donutData, setDonutData] = useState([]); + + useEffect(() => { + const fetchDonutData = async () => { + try { + const response = await axiosInstance.get("/dashboard/stats/"); + const todoCount = response.data.todo_count || 0; + const recurrenceCount = response.data.recurrence_count || 0; + + const donutData = [ + { name: "Todo", count: todoCount }, + { name: "Recurrence", count: recurrenceCount }, + ]; + + setDonutData(donutData); + } catch (error) { + console.error("Error fetching donut data:", error); + } + }; + fetchDonutData(); + }, []); -const donutDataArray = await fetchDonutData(); -export default function DonutChartGraph() { return ( setValue(v)} showAnimation radius={25} /> diff --git a/frontend/src/components/dashboard/KpiCard.jsx b/frontend/src/components/dashboard/KpiCard.jsx index 7ebb841..c5c3165 100644 --- a/frontend/src/components/dashboard/KpiCard.jsx +++ b/frontend/src/components/dashboard/KpiCard.jsx @@ -1,42 +1,57 @@ - import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { useEffect, useState } from "react"; +import { axiosInstance } from "src/api/AxiosConfig"; -const fetchKpiCardData = async () => { - let res = await axiosInstance.get("/dashboard/stats/"); - // let completedThisWeek = res.data["completed_this_week"]; - // let completedLastWeek = res.data["completed_last_week"]; - let completedThisWeek = 4; - let completedLastWeek = 23; - let percentage = (completedThisWeek / completedLastWeek)*100; - let incOrdec = undefined; - if (completedThisWeek <= completedLastWeek) { - incOrdec = "moderateDecrease"; - } - if (completedThisWeek > completedLastWeek) { - incOrdec = "moderateIncrease"; - } - return {completedThisWeek, completedLastWeek, incOrdec, percentage}; -} +export function KpiCard() { + const [kpiCardData, setKpiCardData] = useState({ + completedThisWeek: 0, + completedLastWeek: 0, + incOrdec: undefined, + percentage: 0, + }); -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 ( -
- {completedThisWeek} + {kpiCardData.completedThisWeek}
- {percentage.toFixed(0)}% + {kpiCardData.percentage.toFixed(0)}%
- vs. {completedLastWeek} (last week) + vs. {kpiCardData.completedLastWeek} (last week) - +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/dashboard/ProgressCircle.jsx b/frontend/src/components/dashboard/ProgressCircle.jsx index 4f8382b..683781e 100644 --- a/frontend/src/components/dashboard/ProgressCircle.jsx +++ b/frontend/src/components/dashboard/ProgressCircle.jsx @@ -1,38 +1,39 @@ -import { Card, Flex, ProgressCircle, Text, } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { Card, Flex, ProgressCircle } from "@tremor/react"; +import { useState, useEffect } from "react"; +import { axiosInstance } from "src/api/AxiosConfig"; -const fetchProgressData = async () => { - try { - let res = await axiosInstance.get("/dashboard/stats/"); - // let completedLastWeek = res.data.completed_last_week; - // let assignLastWeek = res.data.tasks_assigned_last_week; - let completedLastWeek = 15; - let assignLastWeek = 35; - if (completedLastWeek === undefined) { - completedLastWeek = 0; - } - if (assignLastWeek === undefined) { - assignLastWeek = 0; - } - return (completedLastWeek / assignLastWeek) * 100; - } catch (error) { - console.error("Error fetching progress data:", error); - return 0; - } -}; +export function ProgressCircleChart() { + const [progressData, setProgressData] = useState(0); + + useEffect(() => { + const fetchProgressData = async () => { + try { + const response = await axiosInstance.get("/dashboard/stats/"); + let completedLastWeek = response.data.completed_last_week || 0; + let assignLastWeek = response.data.tasks_assigned_last_week || 0; + + if (completedLastWeek === undefined) { + completedLastWeek = 0; + } + if (assignLastWeek === undefined) { + assignLastWeek = 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 ( - + {progressData.toFixed(0)} % diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 8670af3..31cc71f 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -1,46 +1,22 @@ -import { - Card, - Grid, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, - Title, - Legend, - DateRangePicker, -} from "@tremor/react"; -import KpiCard from "./kpiCard"; +import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react"; +import { KpiCard } from "./KpiCard"; import { BarChartGraph } from "./Barchart"; -import DonutChartGraph from "./DonutChart"; +import { DonutChartGraph } from "./DonutChart"; import { AreaChartGraph } from "./Areachart"; -import ProgressCircleChart from "./ProgressCircle"; +import { ProgressCircleChart } from "./ProgressCircle"; import { useState } from "react"; -const valueFormatter = (number) => - `$ ${new Intl.NumberFormat("us").format(number).toString()}`; - -export default function Dashboard() { +export function Dashboard() { const [value, setValue] = useState({ from: new Date(2021, 0, 1), to: new Date(2023, 0, 7), }); - console.log(value); return (
Dashboard All of your progress will be shown right here.
- Select Date Range: - -
@@ -64,8 +40,7 @@ export default function Dashboard() { + colors={["indigo"]}> diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx index 80bd445..819a980 100644 --- a/frontend/src/components/kanbanBoard/columnContainer.jsx +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -3,13 +3,13 @@ import { BsFillTrashFill } from "react-icons/bs"; import { AiOutlinePlusCircle } from "react-icons/ai"; import { CSS } from "@dnd-kit/utilities"; 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 tasksIds = useMemo(() => { - return tasks.map(task => task.id); + return tasks.map((task) => task.id); }, [tasks]); const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ @@ -78,12 +78,12 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks updateColumn(column.id, e.target.value)} + onChange={(e) => updateColumn(column.id, e.target.value)} autoFocus onBlur={() => { setEditMode(false); }} - onKeyDown={e => { + onKeyDown={(e) => { if (e.key !== "Enter") return; setEditMode(false); }} @@ -109,7 +109,7 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks {/* Column task container */}
- {tasks.map(task => ( + {tasks.map((task) => ( ))} @@ -126,5 +126,3 @@ function ColumnContainer({ column, deleteColumn, updateColumn, createTask, tasks
); } - -export default ColumnContainer; diff --git a/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx b/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx index 478a529..b14adb1 100644 --- a/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx +++ b/frontend/src/components/kanbanBoard/columnContainerWrapper.jsx @@ -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 (
); } - -export default ColumnContainerCard; diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 2272444..a91b2db 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -1,15 +1,14 @@ 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 { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { createPortal } from "react-dom"; -import TaskCard from "./taskCard"; -import { AiOutlinePlusCircle } from "react-icons/ai"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { TaskCard } from "./taskCard"; +import { axiosInstance } from "src/api/AxiosConfig"; -function KanbanBoard() { +export function KanbanBoard() { 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 [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(() => { const fetchData = async () => { try { const tasksResponse = await axiosInstance.get("/todo"); // Transform - const transformedTasks = tasksResponse.data.map(task => ({ + const transformedTasks = tasksResponse.data.map((task) => ({ id: task.id, columnId: task.list_board, content: task.title, difficulty: task.difficulty, notes: task.notes, importance: task.importance, - difficulty: task.difficulty, challenge: task.challenge, fromSystem: task.fromSystem, creation_date: task.creation_date, @@ -95,7 +59,7 @@ function KanbanBoard() { const columnsResponse = await axiosInstance.get("/lists"); // Transform - const transformedColumns = columnsResponse.data.map(column => ({ + const transformedColumns = columnsResponse.data.map((column) => ({ id: column.id, title: column.name, })); @@ -135,7 +99,7 @@ function KanbanBoard() {
- {columns.map(col => ( + {columns.map((col) => ( task.columnId === col.id)} + tasks={tasks.filter((task) => task.columnId === col.id)} /> ))}
- {/* create new column */} -
{createPortal( @@ -186,7 +125,7 @@ function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={tasks.filter(task => task.columnId === activeColumn.id)} + tasks={tasks.filter((task) => task.columnId === activeColumn.id)} /> )} {activeTask && } @@ -213,35 +152,34 @@ function KanbanBoard() { axiosInstance .post("todo/", newTaskData) - .then(response => { + .then((response) => { const newTask = { id: response.data.id, columnId, content: response.data.title, }; - }) - .catch(error => { + .catch((error) => { console.error("Error creating task:", error); }); - setTasks(tasks => [...tasks, newTask]); - } + setTasks((tasks) => [...tasks, newTask]); + } function deleteTask(id) { - const newTasks = tasks.filter(task => task.id !== id); + const newTasks = tasks.filter((task) => task.id !== id); axiosInstance .delete(`todo/${id}/`) - .then(response => { + .then((response) => { setTasks(newTasks); }) - .catch(error => { + .catch((error) => { console.error("Error deleting Task:", error); }); - setTasks(newTasks); + setTasks(newTasks); } function updateTask(id, content) { - const newTasks = tasks.map(task => { + const newTasks = tasks.map((task) => { if (task.id !== id) return task; return { ...task, content }; }); @@ -252,15 +190,15 @@ function KanbanBoard() { function createNewColumn() { axiosInstance .post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id }) - .then(response => { + .then((response) => { const newColumn = { id: response.data.id, title: response.data.name, }; - setColumns(prevColumns => [...prevColumns, newColumn]); + setColumns((prevColumns) => [...prevColumns, newColumn]); }) - .catch(error => { + .catch((error) => { console.error("Error creating ListBoard:", error); }); } @@ -268,22 +206,22 @@ function KanbanBoard() { function deleteColumn(id) { axiosInstance .delete(`lists/${id}/`) - .then(response => { - setColumns(prevColumns => prevColumns.filter(col => col.id !== id)); + .then((response) => { + setColumns((prevColumns) => prevColumns.filter((col) => col.id !== id)); }) - .catch(error => { + .catch((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 .delete(`todo/${task.id}/`) - .then(response => { - setTasks(prevTasks => prevTasks.filter(t => t.id !== task.id)); + .then((response) => { + setTasks((prevTasks) => prevTasks.filter((t) => t.id !== task.id)); }) - .catch(error => { + .catch((error) => { console.error("Error deleting Task:", error); }); }); @@ -293,10 +231,10 @@ function KanbanBoard() { // Update the column axiosInstance .patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements - .then(response => { - setColumns(prevColumns => prevColumns.map(col => (col.id === id ? { ...col, title } : col))); + .then((response) => { + setColumns((prevColumns) => prevColumns.map((col) => (col.id === id ? { ...col, title } : col))); }) - .catch(error => { + .catch((error) => { console.error("Error updating ListBoard:", error); }); } @@ -330,9 +268,9 @@ function KanbanBoard() { // Reorder columns if the dragged item is a column if (isActiveAColumn && isOverAColumn) { - setColumns(columns => { - const activeColumnIndex = columns.findIndex(col => col.id === activeId); - const overColumnIndex = columns.findIndex(col => col.id === overId); + setColumns((columns) => { + const activeColumnIndex = columns.findIndex((col) => col.id === activeId); + const overColumnIndex = columns.findIndex((col) => col.id === overId); const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex); @@ -342,9 +280,9 @@ function KanbanBoard() { // Reorder tasks within the same column if (isActiveATask && isOverATask) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); - const overIndex = tasks.findIndex(t => t.id === overId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); const reorderedTasks = arrayMove(tasks, activeIndex, overIndex); @@ -354,15 +292,15 @@ function KanbanBoard() { // Move tasks between columns and update columnId if (isActiveATask && isOverAColumn) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); tasks[activeIndex].columnId = overId; axiosInstance .put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 }) - .then(response => {}) - .catch(error => { + .then((response) => {}) + .catch((error) => { console.error("Error updating task columnId:", error); }); @@ -386,9 +324,9 @@ function KanbanBoard() { if (!isActiveATask) return; if (isActiveATask && isOverATask) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); - const overIndex = tasks.findIndex(t => t.id === overId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); if (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"; if (isActiveATask && isOverAColumn) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); tasks[activeIndex].columnId = overId; return arrayMove(tasks, activeIndex, activeIndex); }); } } - - function generateId() { - return Math.floor(Math.random() * 10001); - } } - -export default KanbanBoard; diff --git a/frontend/src/components/kanbanBoard/kanbanPage.jsx b/frontend/src/components/kanbanBoard/kanbanPage.jsx index 37671ae..9225ae7 100644 --- a/frontend/src/components/kanbanBoard/kanbanPage.jsx +++ b/frontend/src/components/kanbanBoard/kanbanPage.jsx @@ -1,12 +1,12 @@ -import KanbanBoard from "./kanbanBoard"; -import React, { useState } from 'react'; +import { KanbanBoard } from "./kanbanBoard"; +import { useState } from "react"; -const KanbanPage = () => { - const [activeTab, setActiveTab] = useState('kanban'); +export const KanbanPage = () => { + const [activeTab, setActiveTab] = useState("kanban"); - const handleTabClick = (tabId) => { - setActiveTab(tabId); - }; + const handleTabClick = (tabId) => { + setActiveTab(tabId); + }; return (
@@ -29,10 +29,7 @@ const KanbanPage = () => {
-
-
+
); }; - -export default KanbanPage; diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 2c5053f..1a151c8 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { BsFillTrashFill } from "react-icons/bs"; import { useSortable } from "@dnd-kit/sortable"; 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 { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ @@ -15,7 +15,6 @@ function TaskCard({ task, deleteTask, updateTask}) { }, }); - const style = { transition, transform: CSS.Transform.toString(transform), @@ -45,7 +44,7 @@ function TaskCard({ task, deleteTask, updateTask}) { description={task.description} tags={task.tags} difficulty={task.difficulty} - challenge={task.challenge} + f challenge={task.challenge} importance={task.importance} />
); } - -export default TaskCard; diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index ddf954b..e151d8f 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; 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 [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); @@ -28,7 +28,8 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo

- {}{title} + {} + {title}

{title}

@@ -45,13 +46,13 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo
-
+
@@ -144,5 +145,3 @@ function TaskDetailModal({ title, description, tags, difficulty, challenge, impo ); } - -export default TaskDetailModal; diff --git a/frontend/src/components/landingPage/LandingPage.jsx b/frontend/src/components/landingPage/LandingPage.jsx new file mode 100644 index 0000000..1b0bd9e --- /dev/null +++ b/frontend/src/components/landingPage/LandingPage.jsx @@ -0,0 +1,44 @@ +import { FloatingParticles } from "../FlaotingParticles"; + +export function LandingPage() { + return ( +
+ {/* Particles Container */} + + {/* Navbar */} +
+
+
+
+

+ Manage your task with{" "} + + TurTask + + +

+

+ Unleash productivity with our personal task and project + management. +

+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/navigations/IconSideNav.jsx b/frontend/src/components/navigations/IconSideNav.jsx index dc364f3..02cc1a0 100644 --- a/frontend/src/components/navigations/IconSideNav.jsx +++ b/frontend/src/components/navigations/IconSideNav.jsx @@ -1,9 +1,9 @@ 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 { IoSettingsOutline } from "react-icons/io5"; import { AnimatePresence, motion } from "framer-motion"; -import { Link, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const menuItems = [ { id: 0, path: "/", icon: }, @@ -13,20 +13,12 @@ const menuItems = [ { id: 4, path: "/priority", icon: }, ]; -const IconSideNav = () => { - return ( -
- -
- ); -}; - -const SideNav = () => { +export const SideNav = () => { const [selected, setSelected] = useState(0); return (
); } -export default NavBar; diff --git a/frontend/src/components/ProfileUpdateComponent.jsx b/frontend/src/components/profile/ProfileUpdateComponent.jsx similarity index 88% rename from frontend/src/components/ProfileUpdateComponent.jsx rename to frontend/src/components/profile/ProfileUpdateComponent.jsx index 74da41f..12f5e98 100644 --- a/frontend/src/components/ProfileUpdateComponent.jsx +++ b/frontend/src/components/profile/ProfileUpdateComponent.jsx @@ -1,7 +1,7 @@ -import React, { useState, useRef } from "react"; -import { ApiUpdateUserProfile } from "../api/UserProfileApi"; +import { useState, useRef } from "react"; +import { ApiUpdateUserProfile } from "src/api/UserProfileApi"; -function ProfileUpdateComponent() { +export function ProfileUpdateComponent() { const [file, setFile] = useState(null); const [username, setUsername] = useState(""); const [fullName, setFullName] = useState(""); @@ -15,7 +15,7 @@ function ProfileUpdateComponent() { } }; - const handleFileChange = e => { + const handleFileChange = (e) => { const selectedFile = e.target.files[0]; if (selectedFile) { setFile(selectedFile); @@ -66,7 +66,7 @@ function ProfileUpdateComponent() { placeholder="Enter your username" className="input w-full" value={username} - onChange={e => setUsername(e.target.value)} + onChange={(e) => setUsername(e.target.value)} />
@@ -78,7 +78,7 @@ function ProfileUpdateComponent() { placeholder="Enter your full name" className="input w-full" value={fullName} - onChange={e => setFullName(e.target.value)} + onChange={(e) => setFullName(e.target.value)} />
@@ -89,7 +89,7 @@ function ProfileUpdateComponent() { placeholder="Tell us about yourself" className="textarea w-full h-32" value={about} - onChange={e => setAbout(e.target.value)} + onChange={(e) => setAbout(e.target.value)} />
@@ -100,5 +100,3 @@ function ProfileUpdateComponent() {
); } - -export default ProfileUpdateComponent; diff --git a/frontend/src/components/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx similarity index 82% rename from frontend/src/components/profilePage.jsx rename to frontend/src/components/profile/profilePage.jsx index 4b0d0b4..d61368a 100644 --- a/frontend/src/components/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -1,7 +1,6 @@ -import * as React from "react"; -import ProfileUpdateComponent from "./ProfileUpdateComponent"; +import { ProfileUpdateComponent } from "./ProfileUpdateComponent"; -function ProfileUpdatePage() { +export function ProfileUpdatePage() { return (
@@ -47,7 +46,7 @@ function ProfileUpdatePage() {
3213/321312321 points
- +
@@ -75,10 +74,10 @@ function ProfileUpdatePage() {
-
-

About me

-
-