├── .env.example ├── .github ├── CODE_OF_CONDUCT.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .idea ├── .gitignore ├── Learning-Management-System.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── Jenkinsfile ├── LICENSE ├── LMS-Backend ├── .env.example ├── .idea │ ├── .gitignore │ ├── LMS-Backend.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── Dockerfile ├── LICENSE ├── LMSBackend │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-312.pyc │ │ ├── settings.cpython-312.pyc │ │ ├── urls.cpython-312.pyc │ │ └── wsgi.cpython-312.pyc │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── README.md ├── core │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-312.pyc │ │ ├── admin.cpython-312.pyc │ │ ├── apps.cpython-312.pyc │ │ ├── models.cpython-312.pyc │ │ ├── serializers.cpython-312.pyc │ │ ├── urls.cpython-312.pyc │ │ └── views.cpython-312.pyc │ ├── admin.py │ ├── apps.py │ ├── management │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ └── __init__.cpython-312.pyc │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ └── seed_sample_data.cpython-312.pyc │ │ │ └── seed_sample_data.py │ ├── migrations │ │ ├── __init__.py │ │ └── __pycache__ │ │ │ └── __init__.cpython-312.pyc │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── db.sqlite3 ├── manage.py └── requirements.txt ├── LMS-Frontend ├── Dockerfile ├── LICENSE ├── README.md └── app │ ├── .editorconfig │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json │ ├── README.md │ ├── angular.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.config.server.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── auth │ │ │ ├── login │ │ │ │ ├── login.component.css │ │ │ │ ├── login.component.html │ │ │ │ └── login.component.ts │ │ │ └── register │ │ │ │ ├── register.component.css │ │ │ │ ├── register.component.html │ │ │ │ └── register.component.ts │ │ ├── components │ │ │ ├── course-list │ │ │ │ ├── course-list.component.css │ │ │ │ ├── course-list.component.html │ │ │ │ └── course-list.component.ts │ │ │ ├── enrollment-list │ │ │ │ ├── enrollment-list.component.css │ │ │ │ ├── enrollment-list.component.html │ │ │ │ └── enrollment-list.component.ts │ │ │ ├── lesson-list │ │ │ │ ├── lesson-list.component.css │ │ │ │ ├── lesson-list.component.html │ │ │ │ └── lesson-list.component.ts │ │ │ ├── progress-list │ │ │ │ ├── progress-list.component.css │ │ │ │ ├── progress-list.component.html │ │ │ │ └── progress-list.component.ts │ │ │ └── user-list │ │ │ │ ├── user-list.component.css │ │ │ │ ├── user-list.component.html │ │ │ │ └── user-list.component.ts │ │ ├── core │ │ │ ├── footer │ │ │ │ ├── footer.component.css │ │ │ │ ├── footer.component.html │ │ │ │ └── footer.component.ts │ │ │ └── header │ │ │ │ ├── header.component.css │ │ │ │ ├── header.component.html │ │ │ │ └── header.component.ts │ │ ├── pages │ │ │ ├── home │ │ │ │ ├── home.component.css │ │ │ │ ├── home.component.html │ │ │ │ └── home.component.ts │ │ │ └── notfound │ │ │ │ ├── notfound.component.css │ │ │ │ ├── notfound.component.html │ │ │ │ └── notfound.component.ts │ │ └── services │ │ │ ├── auth.interceptor.ts │ │ │ ├── auth.service.ts │ │ │ ├── course.service.ts │ │ │ ├── enrollment.service.ts │ │ │ ├── lesson.service.ts │ │ │ ├── progress.service.ts │ │ │ └── user.service.ts │ ├── apple-touch-icon.png │ ├── assets │ │ ├── .gitkeep │ │ ├── icon-192x192.png │ │ └── icon-512x512.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── manifest.json │ └── styles.css │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── README.md ├── docker-compose.yml ├── docs ├── admin-ui.png ├── browsable-api.png ├── course-ui.png ├── enrollments-ui.png ├── footer-ui.png ├── gui-tools.png ├── home-ui.png ├── lesson-ui.png ├── login-ui.png ├── mobile-ui.png ├── notfound.png ├── progress-ui.png ├── redoc-ui.png ├── register-ui.png ├── swagger-ui.png ├── unauthorized-ui.png └── user-ui.png ├── identifier.sqlite ├── kubernetes ├── backend-deployment.yaml ├── backend-service.yaml ├── configmap.yaml ├── frontend-deployment.yaml └── frontend-service.yaml ├── nginx ├── Dockerfile ├── docker-compose.yml ├── nginx.conf └── start_nginx.sh ├── openapi.yaml ├── package-lock.json └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY 2 | DJANGO_DEBUG 3 | DJANGO_ALLOWED_HOSTS 4 | MONGO_DB_URI 5 | MONGO_DB_USERNAME 6 | MONGO_DB_PASSWORD 7 | REDIS_LOCATION 8 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Introduction 4 | 5 | Welcome to this Open-Source Project! This Code of Conduct outlines our expectations for participants within the community, as well as steps to report unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our Code of Conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - **Using welcoming and inclusive language** 12 | - **Being respectful of differing viewpoints and experiences** 13 | - **Gracefully accepting constructive criticism** 14 | - **Focusing on what is best for the community** 15 | - **Showing empathy towards other community members** 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - **The use of sexualized language or imagery and unwelcome sexual attention or advances** 20 | - **Trolling, insulting/derogatory comments, and personal or political attacks** 21 | - **Public or private harassment** 22 | - **Publishing others' private information, such as a physical or electronic address, without explicit permission** 23 | - **Other conduct which could reasonably be considered inappropriate in a professional setting** 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [insert email address]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]. 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | --- 48 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Overview 4 | Briefly describe the purpose of this pull request. What features or bug fixes are included? 5 | 6 | ## Related Issue/Ticket 7 | Link any related issue or ticket here. 8 | 9 | ## Files Changed 10 | List the files that have been added, deleted, or modified. 11 | 12 | ## Testing 13 | Describe how you tested these changes. 14 | 15 | ## Screenshots (if applicable) 16 | Include screenshots or GIFs if you've made UI changes. 17 | 18 | ## Notes to Reviewers 19 | Add any notes for reviewers, such as areas to focus on, decisions you made, etc. 20 | 21 | ## Deployment Notes 22 | Include any necessary steps for deployment, configuration changes, etc. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /LMS-Backend/.env 2 | /venv/ 3 | /node_modules/ 4 | /.env 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/Learning-Management-System.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Install Dependencies') { 5 | steps { 6 | sh 'npm install' 7 | } 8 | } 9 | stage('Build') { 10 | steps { 11 | sh 'npm run build' 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Son Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LMS-Backend/.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY 2 | DJANGO_DEBUG 3 | DJANGO_ALLOWED_HOSTS 4 | MONGO_DB_URI 5 | MONGO_DB_USERNAME 6 | MONGO_DB_PASSWORD 7 | REDIS_LOCATION 8 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/LMS-Backend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 105 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LMS-Backend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LMS-Backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory 9 | WORKDIR /app 10 | 11 | # Install system dependencies 12 | RUN apt-get update && apt-get install -y \ 13 | build-essential \ 14 | libpq-dev \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # Install Python dependencies 18 | COPY requirements.txt . 19 | RUN pip install --no-cache-dir -r requirements.txt 20 | 21 | # Copy the project files 22 | COPY . . 23 | 24 | # Expose the port 25 | EXPOSE 8000 26 | 27 | # Run the Django development server 28 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] 29 | -------------------------------------------------------------------------------- /LMS-Backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Son Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/LMSBackend/__init__.py -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/LMSBackend/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/__pycache__/settings.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/LMSBackend/__pycache__/settings.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/LMSBackend/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/__pycache__/wsgi.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/LMSBackend/__pycache__/wsgi.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for LMSBackend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'LMSBackend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for LMSBackend project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | from datetime import timedelta 13 | from pathlib import Path 14 | from decouple import config 15 | from mongoengine import connect 16 | 17 | 18 | # Connect to MongoDB 19 | connect( 20 | db="lms_database", 21 | host=config('MONGO_DB_URI'), 22 | username=config('MONGO_DB_USERNAME'), 23 | password=config('MONGO_DB_PASSWORD'), 24 | authentication_source='admin', 25 | ssl=True 26 | ) 27 | 28 | 29 | # SECURITY WARNING: keep the secret key used in production secret! 30 | SECRET_KEY = config('DJANGO_SECRET_KEY') 31 | 32 | # SECURITY WARNING: don't run with debug turned on in production! 33 | DEBUG = config('DJANGO_DEBUG', default=False, cast=bool) 34 | 35 | 36 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 37 | BASE_DIR = Path(__file__).resolve().parent.parent 38 | 39 | 40 | # Quick-start development settings - unsuitable for production 41 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 42 | 43 | ALLOWED_HOSTS = ['*'] 44 | 45 | 46 | # Application definition 47 | 48 | INSTALLED_APPS = [ 49 | 'django.contrib.admin', 50 | 'django.contrib.auth', 51 | 'django.contrib.contenttypes', 52 | 'django.contrib.sessions', 53 | 'django.contrib.messages', 54 | 'django.contrib.staticfiles', 55 | 'rest_framework', 56 | 'rest_framework.authtoken', 57 | 'rest_framework_mongoengine', 58 | 'core', 59 | 'corsheaders', 60 | 'drf_yasg', 61 | 'dj_rest_auth', 62 | 'dj_rest_auth.registration', 63 | 'allauth', 64 | 'allauth.account', 65 | 'allauth.socialaccount', 66 | 'django.contrib.sites', 67 | ] 68 | 69 | SITE_ID = 1 70 | 71 | DATABASES = { 72 | 'default': { 73 | 'ENGINE': 'django.db.backends.sqlite3', 74 | 'NAME': BASE_DIR / 'db.sqlite3', 75 | } 76 | } 77 | 78 | MIDDLEWARE = [ 79 | 'django.middleware.security.SecurityMiddleware', 80 | 'django.contrib.sessions.middleware.SessionMiddleware', 81 | 'django.middleware.common.CommonMiddleware', 82 | 'django.middleware.csrf.CsrfViewMiddleware', 83 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 84 | 'django.contrib.messages.middleware.MessageMiddleware', 85 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 86 | 'allauth.account.middleware.AccountMiddleware', 87 | 'corsheaders.middleware.CorsMiddleware', 88 | ] 89 | 90 | ROOT_URLCONF = 'LMSBackend.urls' 91 | 92 | # CORS settings 93 | CORS_ALLOW_ALL_ORIGINS = True # Allow all origins 94 | 95 | # Explicitly allow specific origins 96 | # CORS_ALLOWED_ORIGINS = [ 97 | # 'https://learning-management-system-fullstack.vercel.app', # Frontend hosted on Vercel 98 | # 'https://fullstack-learning-management-system.netlify.app', # Backup Frontend hosted on Netlify 99 | # 'http://localhost:4200', # Local development 100 | # ] 101 | 102 | CORS_ALLOW_CREDENTIALS = True # Allow sending credentials like cookies and auth headers 103 | 104 | TEMPLATES = [ 105 | { 106 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 107 | 'DIRS': [], 108 | 'APP_DIRS': True, 109 | 'OPTIONS': { 110 | 'context_processors': [ 111 | 'django.template.context_processors.debug', 112 | 'django.template.context_processors.request', 113 | 'django.contrib.auth.context_processors.auth', 114 | 'django.contrib.messages.context_processors.messages', 115 | ], 116 | }, 117 | }, 118 | ] 119 | 120 | WSGI_APPLICATION = 'LMSBackend.wsgi.application' 121 | 122 | CACHES = { 123 | 'default': { 124 | 'BACKEND': 'django_redis.cache.RedisCache', 125 | 'LOCATION': config('REDIS_LOCATION'), 126 | 'OPTIONS': { 127 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 128 | } 129 | } 130 | } 131 | 132 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 133 | SESSION_CACHE_ALIAS = "default" 134 | 135 | REST_FRAMEWORK = { 136 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 137 | 'rest_framework_simplejwt.authentication.JWTAuthentication', # JWT Authentication 138 | 'rest_framework.authentication.TokenAuthentication', 139 | ], 140 | 'DEFAULT_PERMISSION_CLASSES': [ 141 | 'rest_framework.permissions.IsAuthenticated', 142 | ], 143 | } 144 | 145 | SWAGGER_SETTINGS = { 146 | 'USE_SESSION_AUTH': False, 147 | 'SECURITY_DEFINITIONS': { 148 | 'Bearer': { 149 | 'type': 'apiKey', 150 | 'in': 'header', 151 | 'name': 'Authorization', 152 | 'description': 'Enter your token in the format: Token {your_token}' 153 | } 154 | }, 155 | 'DEFAULT_API_KEY': 'Token ', 156 | 'APIS_SORTER': 'alpha', 157 | 'OPERATIONS_SORTER': 'alpha', 158 | 'SHOW_REQUEST_HEADERS': True, 159 | } 160 | 161 | SIMPLE_JWT = { 162 | 'ACCESS_TOKEN_LIFETIME': timedelta(days=7), 163 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=14), 164 | } 165 | 166 | ACCOUNT_EMAIL_VERIFICATION = 'none' 167 | ACCOUNT_AUTHENTICATION_METHOD = 'username' 168 | ACCOUNT_EMAIL_REQUIRED = False 169 | 170 | 171 | # Password validation 172 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 173 | 174 | AUTH_PASSWORD_VALIDATORS = [ 175 | { 176 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 177 | }, 178 | { 179 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 180 | }, 181 | { 182 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 183 | }, 184 | { 185 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 186 | }, 187 | ] 188 | 189 | 190 | # Internationalization 191 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 192 | 193 | LANGUAGE_CODE = 'en-us' 194 | 195 | TIME_ZONE = 'UTC' 196 | 197 | USE_I18N = True 198 | 199 | USE_TZ = True 200 | 201 | 202 | # Static files (CSS, JavaScript, Images) 203 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 204 | 205 | STATIC_URL = 'static/' 206 | 207 | 208 | # Default primary key field type 209 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 210 | 211 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 212 | -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from django.views.generic import RedirectView 4 | from rest_framework import permissions 5 | from drf_yasg.views import get_schema_view 6 | from drf_yasg import openapi 7 | 8 | schema_view = get_schema_view( 9 | openapi.Info( 10 | title="Learning Management System API", 11 | default_version='v1', 12 | description="Comprehensive API documentation for the Learning Management System", 13 | terms_of_service="https://learning-management-system-fullstack.vercel.app/", 14 | contact=openapi.Contact(email="hoangson091104@gmail.com", name="Learning Management System", url="https://learning-management-system-fullstack.vercel.app/"), 15 | license=openapi.License(name="MIT License"), 16 | ), 17 | public=True, 18 | permission_classes=[permissions.AllowAny], 19 | ) 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('api/', include('core.urls')), 24 | path('api/auth/', include('dj_rest_auth.urls')), 25 | path('api/auth/registration/', include('dj_rest_auth.registration.urls')), 26 | path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), 27 | path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), 28 | path('', RedirectView.as_view(url='/swagger', permanent=False)), # Redirect base URL to Swagger UI URL 29 | ] 30 | -------------------------------------------------------------------------------- /LMS-Backend/LMSBackend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for LMSBackend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'LMSBackend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /LMS-Backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__init__.py -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/serializers.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/serializers.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | admin.site.site_header = _("Learning Management System Administration") 5 | admin.site.site_title = _("Learning Management System Admin") 6 | admin.site.index_title = _("Welcome to the Learning Management System Admin") 7 | -------------------------------------------------------------------------------- /LMS-Backend/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'core' 7 | -------------------------------------------------------------------------------- /LMS-Backend/core/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/management/__init__.py -------------------------------------------------------------------------------- /LMS-Backend/core/management/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/management/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/management/commands/__init__.py -------------------------------------------------------------------------------- /LMS-Backend/core/management/commands/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/management/commands/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/management/commands/__pycache__/seed_sample_data.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/management/commands/__pycache__/seed_sample_data.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/management/commands/seed_sample_data.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from core.models import User, Course, Category, Lesson, Quiz, Question, Choice, Enrollment, Progress, Notification 3 | from datetime import datetime 4 | from faker import Faker 5 | import random 6 | 7 | 8 | class Command(BaseCommand): 9 | help = 'Seed realistic sample data for all API endpoints' 10 | 11 | def handle(self, *args, **kwargs): 12 | # Initialize Faker 13 | fake = Faker() 14 | 15 | # Clear existing data 16 | User.drop_collection() 17 | Category.drop_collection() 18 | Course.drop_collection() 19 | Lesson.drop_collection() 20 | Quiz.drop_collection() 21 | Question.drop_collection() 22 | Choice.drop_collection() 23 | Enrollment.drop_collection() 24 | Progress.drop_collection() 25 | Notification.drop_collection() 26 | 27 | # Seed Users 28 | users = [ 29 | User( 30 | username=fake.user_name(), 31 | email=fake.email(), 32 | is_instructor=random.choice([True, False]), 33 | is_student=random.choice([True, False]), 34 | bio=fake.paragraph(nb_sentences=2), 35 | profile_picture=fake.image_url() 36 | ).save() 37 | for _ in range(10) 38 | ] 39 | 40 | # Seed Categories 41 | categories = [ 42 | Category( 43 | name=fake.word().capitalize(), 44 | description=fake.sentence(nb_words=6) 45 | ).save() 46 | for _ in range(5) 47 | ] 48 | 49 | # Seed Courses 50 | courses = [ 51 | Course( 52 | title=fake.catch_phrase(), 53 | description=fake.paragraph(nb_sentences=5), 54 | instructor=random.choice(users), 55 | category=random.choice(categories), 56 | created_at=datetime.now(), 57 | updated_at=datetime.now(), 58 | image=fake.image_url(), 59 | price=random.uniform(50, 500), 60 | published=random.choice([True, False]) 61 | ).save() 62 | for _ in range(8) 63 | ] 64 | 65 | # Seed Lessons 66 | lessons = [ 67 | Lesson( 68 | title=fake.sentence(nb_words=4), 69 | course=random.choice(courses), 70 | content=fake.text(max_nb_chars=200), 71 | video_url=fake.url(), 72 | created_at=datetime.now(), 73 | updated_at=datetime.now() 74 | ).save() 75 | for _ in range(15) 76 | ] 77 | 78 | # Seed Quizzes 79 | quizzes = [ 80 | Quiz( 81 | lesson=random.choice(lessons), 82 | title=fake.sentence(nb_words=3), 83 | created_at=datetime.now() 84 | ).save() 85 | for _ in range(10) 86 | ] 87 | 88 | # Seed Questions 89 | questions = [ 90 | Question( 91 | quiz=random.choice(quizzes), 92 | text=fake.sentence(nb_words=10) + "?", 93 | answer=fake.word(), 94 | created_at=datetime.now() 95 | ).save() 96 | for _ in range(20) 97 | ] 98 | 99 | # Seed Choices 100 | choices = [ 101 | Choice( 102 | question=random.choice(questions), 103 | text=fake.sentence(nb_words=4), 104 | is_correct=random.choice([True, False]) 105 | ).save() 106 | for _ in range(40) 107 | ] 108 | 109 | # Seed Enrollments 110 | enrollments = [ 111 | Enrollment( 112 | student=random.choice(users), 113 | course=random.choice(courses), 114 | enrolled_at=datetime.now() 115 | ).save() 116 | for _ in range(10) 117 | ] 118 | 119 | # Seed Progress 120 | progress_records = [ 121 | Progress( 122 | student=random.choice(users), 123 | lesson=random.choice(lessons), 124 | completed=random.choice([True, False]), 125 | completed_at=datetime.now() if random.choice([True, False]) else None 126 | ).save() 127 | for _ in range(10) 128 | ] 129 | 130 | # Seed Notifications 131 | notifications = [ 132 | Notification( 133 | recipient=random.choice(users), 134 | message=fake.sentence(nb_words=6), 135 | created_at=datetime.now(), 136 | is_read=random.choice([True, False]) 137 | ).save() 138 | for _ in range(10) 139 | ] 140 | 141 | self.stdout.write(self.style.SUCCESS('Successfully seeded realistic sample data')) 142 | -------------------------------------------------------------------------------- /LMS-Backend/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/migrations/__init__.py -------------------------------------------------------------------------------- /LMS-Backend/core/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/core/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /LMS-Backend/core/models.py: -------------------------------------------------------------------------------- 1 | from mongoengine import Document, fields 2 | 3 | 4 | class User(Document): 5 | username = fields.StringField(required=True, max_length=100, unique=True) 6 | email = fields.EmailField(required=True, unique=True) 7 | is_instructor = fields.BooleanField(default=False) 8 | is_student = fields.BooleanField(default=True) 9 | bio = fields.StringField() 10 | profile_picture = fields.StringField() 11 | 12 | meta = {'collection': 'users'} 13 | 14 | 15 | class Category(Document): 16 | name = fields.StringField(max_length=100) 17 | description = fields.StringField() 18 | 19 | meta = {'collection': 'categories'} 20 | 21 | 22 | class Course(Document): 23 | title = fields.StringField(max_length=255) 24 | description = fields.StringField() 25 | instructor = fields.ReferenceField(User, reverse_delete_rule='CASCADE') 26 | category = fields.ReferenceField(Category, reverse_delete_rule='NULLIFY') 27 | created_at = fields.DateTimeField() 28 | updated_at = fields.DateTimeField() 29 | image = fields.StringField() 30 | price = fields.DecimalField(min_value=0) 31 | published = fields.BooleanField(default=False) 32 | 33 | meta = {'collection': 'courses'} 34 | 35 | 36 | class Lesson(Document): 37 | title = fields.StringField(max_length=255) 38 | course = fields.ReferenceField(Course, reverse_delete_rule='CASCADE') 39 | content = fields.StringField() 40 | video_url = fields.StringField() 41 | created_at = fields.DateTimeField() 42 | updated_at = fields.DateTimeField() 43 | 44 | meta = {'collection': 'lessons'} 45 | 46 | 47 | class Quiz(Document): 48 | lesson = fields.ReferenceField(Lesson, reverse_delete_rule='CASCADE') 49 | title = fields.StringField(max_length=255) 50 | created_at = fields.DateTimeField() 51 | 52 | meta = {'collection': 'quizzes'} 53 | 54 | 55 | class Question(Document): 56 | quiz = fields.ReferenceField(Quiz, reverse_delete_rule='CASCADE') 57 | text = fields.StringField(max_length=1024) 58 | answer = fields.StringField(max_length=1024) 59 | created_at = fields.DateTimeField() 60 | 61 | meta = {'collection': 'questions'} 62 | 63 | 64 | class Choice(Document): 65 | question = fields.ReferenceField(Question, reverse_delete_rule='CASCADE') 66 | text = fields.StringField(max_length=255) 67 | is_correct = fields.BooleanField(default=False) 68 | 69 | meta = {'collection': 'choices'} 70 | 71 | 72 | class Enrollment(Document): 73 | student = fields.ReferenceField(User, reverse_delete_rule='CASCADE') 74 | course = fields.ReferenceField(Course, reverse_delete_rule='CASCADE') 75 | enrolled_at = fields.DateTimeField() 76 | 77 | meta = {'collection': 'enrollments'} 78 | 79 | 80 | class Progress(Document): 81 | student = fields.ReferenceField(User, reverse_delete_rule='CASCADE') 82 | lesson = fields.ReferenceField(Lesson, reverse_delete_rule='CASCADE') 83 | completed = fields.BooleanField(default=False) 84 | completed_at = fields.DateTimeField() 85 | 86 | meta = {'collection': 'progress'} 87 | 88 | 89 | class Notification(Document): 90 | recipient = fields.ReferenceField(User, reverse_delete_rule='CASCADE') 91 | message = fields.StringField(max_length=255) 92 | created_at = fields.DateTimeField() 93 | is_read = fields.BooleanField(default=False) 94 | 95 | meta = {'collection': 'notifications'} 96 | -------------------------------------------------------------------------------- /LMS-Backend/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework_mongoengine.serializers import DocumentSerializer 2 | from .models import User, Category, Course, Lesson, Quiz, Question, Choice, Enrollment, Progress, Notification 3 | 4 | 5 | class UserSerializer(DocumentSerializer): 6 | class Meta: 7 | model = User 8 | fields = '__all__' 9 | 10 | 11 | class CategorySerializer(DocumentSerializer): 12 | class Meta: 13 | model = Category 14 | fields = '__all__' 15 | 16 | 17 | class CourseSerializer(DocumentSerializer): 18 | class Meta: 19 | model = Course 20 | fields = '__all__' 21 | 22 | 23 | class LessonSerializer(DocumentSerializer): 24 | class Meta: 25 | model = Lesson 26 | fields = '__all__' 27 | 28 | 29 | class QuizSerializer(DocumentSerializer): 30 | class Meta: 31 | model = Quiz 32 | fields = '__all__' 33 | 34 | 35 | class QuestionSerializer(DocumentSerializer): 36 | class Meta: 37 | model = Question 38 | fields = '__all__' 39 | 40 | 41 | class ChoiceSerializer(DocumentSerializer): 42 | class Meta: 43 | model = Choice 44 | fields = '__all__' 45 | 46 | 47 | class EnrollmentSerializer(DocumentSerializer): 48 | class Meta: 49 | model = Enrollment 50 | fields = '__all__' 51 | 52 | 53 | class ProgressSerializer(DocumentSerializer): 54 | class Meta: 55 | model = Progress 56 | fields = '__all__' 57 | 58 | 59 | class NotificationSerializer(DocumentSerializer): 60 | class Meta: 61 | model = Notification 62 | fields = '__all__' 63 | -------------------------------------------------------------------------------- /LMS-Backend/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from rest_framework.test import APIClient 3 | from rest_framework import status 4 | from mongoengine import connect, disconnect 5 | from .models import User, Category, Course, Lesson, Quiz, Question, Choice, Enrollment, Progress, Notification 6 | 7 | 8 | class BaseAPITestCase(TestCase): 9 | """ 10 | Base test case to handle setup and teardown for MongoDB. 11 | """ 12 | 13 | def setUp(self): 14 | # Connect to test database 15 | connect('mongoenginetest', host='mongomock://localhost') 16 | 17 | self.client = APIClient() 18 | 19 | def tearDown(self): 20 | disconnect() 21 | 22 | 23 | class UserModelTest(BaseAPITestCase): 24 | def test_create_user(self): 25 | user = User(username='testuser', email='test@example.com', is_instructor=True, bio='An experienced instructor') 26 | user.save() 27 | 28 | # Check if user is saved correctly 29 | self.assertEqual(User.objects.count(), 1) 30 | self.assertEqual(user.username, 'testuser') 31 | self.assertTrue(user.is_instructor) 32 | 33 | 34 | class CourseModelTest(BaseAPITestCase): 35 | def test_create_course(self): 36 | instructor = User(username='instructor', email='instructor@example.com', is_instructor=True) 37 | instructor.save() 38 | 39 | category = Category(name='Programming', description='Learn programming languages') 40 | category.save() 41 | 42 | course = Course(title='Python Basics', description='Learn the basics of Python', instructor=instructor, 43 | category=category) 44 | course.save() 45 | 46 | self.assertEqual(Course.objects.count(), 1) 47 | self.assertEqual(course.title, 'Python Basics') 48 | self.assertEqual(course.instructor.username, 'instructor') 49 | 50 | 51 | class UserViewSetTest(BaseAPITestCase): 52 | def test_user_list(self): 53 | User(username='user1', email='user1@example.com').save() 54 | User(username='user2', email='user2@example.com').save() 55 | 56 | response = self.client.get('/users/') 57 | 58 | self.assertEqual(response.status_code, status.HTTP_200_OK) 59 | self.assertEqual(len(response.data), 2) 60 | 61 | def test_user_create(self): 62 | user_data = { 63 | "username": "testuser", 64 | "email": "testuser@example.com", 65 | "is_instructor": True, 66 | "bio": "An experienced instructor" 67 | } 68 | response = self.client.post('/users/', user_data, format='json') 69 | 70 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 71 | self.assertEqual(User.objects.count(), 1) 72 | self.assertEqual(User.objects.first().username, 'testuser') 73 | 74 | 75 | class CourseViewSetTest(BaseAPITestCase): 76 | def test_course_list(self): 77 | instructor = User(username='instructor', email='instructor@example.com', is_instructor=True) 78 | instructor.save() 79 | 80 | category = Category(name='Programming', description='Learn programming languages') 81 | category.save() 82 | 83 | Course(title='Python Basics', description='Learn Python', instructor=instructor, category=category).save() 84 | Course(title='Django Basics', description='Learn Django', instructor=instructor, category=category).save() 85 | 86 | response = self.client.get('/courses/') 87 | 88 | self.assertEqual(response.status_code, status.HTTP_200_OK) 89 | self.assertEqual(len(response.data), 2) 90 | 91 | def test_course_create(self): 92 | instructor = User(username='instructor', email='instructor@example.com', is_instructor=True) 93 | instructor.save() 94 | 95 | category = Category(name='Programming', description='Learn programming languages') 96 | category.save() 97 | 98 | course_data = { 99 | "title": "JavaScript Basics", 100 | "description": "Learn JavaScript", 101 | "instructor": str(instructor.id), 102 | "category": str(category.id), 103 | "price": 19.99 104 | } 105 | response = self.client.post('/courses/', course_data, format='json') 106 | 107 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 108 | self.assertEqual(Course.objects.count(), 1) 109 | self.assertEqual(Course.objects.first().title, 'JavaScript Basics') 110 | 111 | 112 | class LessonViewSetTest(BaseAPITestCase): 113 | def test_lesson_create(self): 114 | instructor = User(username='instructor', email='instructor@example.com', is_instructor=True) 115 | instructor.save() 116 | 117 | category = Category(name='Programming', description='Learn programming languages') 118 | category.save() 119 | 120 | course = Course(title='Python Basics', description='Learn Python', instructor=instructor, category=category) 121 | course.save() 122 | 123 | lesson_data = { 124 | "title": "Introduction to Python", 125 | "course": str(course.id), 126 | "content": "This is an introduction to Python programming." 127 | } 128 | response = self.client.post('/lessons/', lesson_data, format='json') 129 | 130 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 131 | self.assertEqual(Lesson.objects.count(), 1) 132 | self.assertEqual(Lesson.objects.first().title, 'Introduction to Python') 133 | 134 | 135 | class QuizViewSetTest(BaseAPITestCase): 136 | def test_quiz_create(self): 137 | instructor = User(username='instructor', email='instructor@example.com', is_instructor=True) 138 | instructor.save() 139 | 140 | category = Category(name='Programming', description='Learn programming languages') 141 | category.save() 142 | 143 | course = Course(title='Python Basics', description='Learn Python', instructor=instructor, category=category) 144 | course.save() 145 | 146 | lesson = Lesson(title='Introduction to Python', course=course, content='Introduction content') 147 | lesson.save() 148 | 149 | quiz_data = { 150 | "title": "Python Basics Quiz", 151 | "lesson": str(lesson.id) 152 | } 153 | response = self.client.post('/quizzes/', quiz_data, format='json') 154 | 155 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 156 | self.assertEqual(Quiz.objects.count(), 1) 157 | self.assertEqual(Quiz.objects.first().title, 'Python Basics Quiz') 158 | -------------------------------------------------------------------------------- /LMS-Backend/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | from .views import (UserViewSet, CourseViewSet, CategoryViewSet, LessonViewSet, QuizViewSet, 4 | QuestionViewSet, ChoiceViewSet, EnrollmentViewSet, ProgressViewSet, NotificationViewSet) 5 | 6 | router = DefaultRouter() 7 | router.register(r'users', UserViewSet, basename='user') 8 | router.register(r'courses', CourseViewSet, basename='course') 9 | router.register(r'categories', CategoryViewSet, basename='category') 10 | router.register(r'lessons', LessonViewSet, basename='lesson') 11 | router.register(r'quizzes', QuizViewSet, basename='quiz') 12 | router.register(r'questions', QuestionViewSet, basename='question') 13 | router.register(r'choices', ChoiceViewSet, basename='choice') 14 | router.register(r'enrollments', EnrollmentViewSet, basename='enrollment') 15 | router.register(r'progress', ProgressViewSet, basename='progress') 16 | router.register(r'notifications', NotificationViewSet, basename='notification') 17 | 18 | urlpatterns = [ 19 | path('', include(router.urls)), 20 | ] 21 | -------------------------------------------------------------------------------- /LMS-Backend/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Backend/db.sqlite3 -------------------------------------------------------------------------------- /LMS-Backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'LMSBackend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | # Seed sample data 13 | execute_from_command_line(['manage.py', 'seed_sample_data']) 14 | except ImportError as exc: 15 | raise ImportError( 16 | "Couldn't import Django. Are you sure it's installed and " 17 | "available on your PYTHONPATH environment variable? Did you " 18 | "forget to activate a virtual environment?" 19 | ) from exc 20 | execute_from_command_line(sys.argv) 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /LMS-Backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | certifi==2024.8.30 3 | charset-normalizer==3.3.2 4 | dataclasses==0.6 5 | dj-rest-auth==6.0.0 6 | Django==4.2.16 7 | django-allauth==64.2.1 8 | django-cors-headers==4.4.0 9 | django-mongoengine==0.5.6 10 | django-redis==5.4.0 11 | django-rest-framework-mongoengine==3.4.1 12 | djangorestframework==3.15.2 13 | djangorestframework-simplejwt==5.3.1 14 | djongo==1.2.31 15 | dnspython==2.6.1 16 | drf-yasg==1.21.8 17 | Faker==28.4.1 18 | idna==3.8 19 | inflection==0.5.1 20 | mongoengine==0.29.0 21 | packaging==24.1 22 | PyJWT==2.9.0 23 | pymongo==4.8.0 24 | python-dateutil==2.9.0.post0 25 | python-decouple==3.8 26 | pytz==2024.1 27 | PyYAML==6.0.2 28 | redis==5.0.8 29 | requests==2.32.3 30 | setuptools==74.1.2 31 | six==1.16.0 32 | sqlparse==0.5.1 33 | uritemplate==4.1.1 34 | urllib3==2.2.2 35 | -------------------------------------------------------------------------------- /LMS-Frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.19 2 | 3 | # Set the working directory 4 | WORKDIR /app 5 | 6 | # Copy package.json and package-lock.json from the Angular app directory 7 | COPY ./app/package*.json ./ 8 | 9 | # Install Angular CLI globally 10 | RUN npm install -g @angular/cli 11 | 12 | # Install frontend dependencies 13 | RUN npm install 14 | 15 | # Copy the entire Angular app to the working directory 16 | COPY ./app . 17 | 18 | # Build the Angular application for production 19 | RUN ng build --configuration production 20 | 21 | # Install a lightweight HTTP server to serve the application 22 | RUN npm install -g http-server 23 | 24 | # Expose the port the application will run on 25 | EXPOSE 8080 26 | 27 | # Serve the Angular application 28 | CMD ["http-server", "dist/your-angular-app-name", "-p", "8080"] 29 | -------------------------------------------------------------------------------- /LMS-Frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Son Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LMS-Frontend/README.md: -------------------------------------------------------------------------------- 1 | # E-Learning Management System Frontend 2 | 3 | The **E-Learning Management System Frontend** is the frontend component of the E-Learning Management System, a web-based application that allows administrators and users to interact with the e-learning platform's various components. The frontend is developed with Angular, and the backend is built using Django REST Framework. 4 | 5 | ## Table of Contents 6 | 7 | - [Project Overview](#project-overview) 8 | - [Features](#features) 9 | - [User Interfaces](#user-interfaces) 10 | - [File Structure](#file-structure) 11 | - [Getting Started](#getting-started) 12 | - [Prerequisites](#prerequisites) 13 | - [Backend Setup (Django)](#backend-setup) 14 | - [Frontend Setup (Angular)](#frontend-setup) 15 | - [Styling and Design](#styling-and-design) 16 | - [Troubleshooting](#troubleshooting) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | - [Contact](#contact) 20 | 21 | ## Project Overview 22 | 23 | This frontend project is built with Angular, a popular web application framework developed by Google. It provides a user-friendly interface for managing courses, lessons, users, enrollments, and progress tracking. The frontend interacts with the backend APIs defined by Django to fetch and update data, handle user authentication, and display dynamic content. 24 | 25 | ## Features 26 | 27 | - **User Authentication**: Token-based login system. 28 | - **Course Management**: Display, create, update, and delete courses. 29 | - **Lesson Management**: Display, create, update, and delete lessons. 30 | - **User Management**: List and manage users. 31 | - **Enrollment Management**: Handle course enrollments. 32 | - **Progress Tracking**: Monitor user progress across lessons. 33 | - **Responsive Design**: Optimized for various devices. 34 | - **Charts and Data Visualization**: Implemented with Chart.js for dynamic data visualization. 35 | 36 | ## User Interfaces 37 | 38 | **Home Page**: 39 | 40 |

41 | Home Page 42 |

43 | 44 | **Course List**: 45 | 46 |

47 | Course List 48 |

49 | 50 | **Lesson List**: 51 | 52 |

53 | Lesson List 54 |

55 | 56 | **User List**: 57 | 58 |

59 | User List 60 |

61 | 62 | **Enrollment List**: 63 | 64 |

65 | Enrollment List 66 |

67 | 68 | **Progress List**: 69 | 70 |

71 | Progress List 72 |

73 | 74 | **Login Page**: 75 | 76 |

77 | Login Page 78 |

79 | 80 | ## File Structure 81 | 82 | The repository is organized as follows: 83 | 84 | ```plaintext 85 | Learning-Management-System/ 86 | ├── LMS-Backend 87 | │ ├── (Backend code) 88 | │ └── ... 89 | ├── LMS-Frontend 90 | │ ├── angular.json 91 | │ ├── package.json 92 | │ ├── README.md 93 | │ ├── LICENSE 94 | │ ├── app/ 95 | │ │ ├── src/ 96 | │ │ │ ├── app/ 97 | │ │ │ │ ├── auth/ 98 | │ │ │ │ │ ├── login/ 99 | │ │ │ │ │ │ ├── login.component.ts 100 | │ │ │ │ │ │ ├── login.component.html 101 | │ │ │ │ │ │ └── login.component.css 102 | │ │ │ │ ├── core/ 103 | │ │ │ │ │ ├── footer/ 104 | │ │ │ │ │ │ ├── footer.component.ts 105 | │ │ │ │ │ │ ├── footer.component.html 106 | │ │ │ │ │ │ └── footer.component.css 107 | │ │ │ │ │ ├── header/ 108 | │ │ │ │ │ │ ├── header.component.ts 109 | │ │ │ │ │ │ ├── header.component.html 110 | │ │ │ │ │ │ └── header.component.css 111 | │ │ │ │ ├── pages/ 112 | │ │ │ │ │ ├── home/ 113 | │ │ │ │ │ │ ├── notfound.component.ts 114 | │ │ │ │ │ │ ├── notfound.component.html 115 | │ │ │ │ │ │ └── home.component.css 116 | │ │ │ │ ├── components/ 117 | │ │ │ │ │ ├── course-list/ 118 | │ │ │ │ │ │ ├── course-list.component.ts 119 | │ │ │ │ │ │ ├── course-list.component.html 120 | │ │ │ │ │ │ └── course-list.component.css 121 | │ │ │ │ │ ├── lesson-list/ 122 | │ │ │ │ │ │ ├── lesson-list.component.ts 123 | │ │ │ │ │ │ ├── lesson-list.component.html 124 | │ │ │ │ │ │ └── lesson-list.component.css 125 | │ │ │ │ │ ├── user-list/ 126 | │ │ │ │ │ │ ├── user-list.component.ts 127 | │ │ │ │ │ │ ├── user-list.component.html 128 | │ │ │ │ │ │ └── user-list.component.css 129 | │ │ │ │ │ ├── enrollment-list/ 130 | │ │ │ │ │ │ ├── enrollment-list.component.ts 131 | │ │ │ │ │ │ ├── enrollment-list.component.html 132 | │ │ │ │ │ │ └── enrollment-list.component.css 133 | │ │ │ │ │ ├── progress-list/ 134 | │ │ │ │ │ │ ├── progress-list.component.ts 135 | │ │ │ │ │ │ ├── progress-list.component.html 136 | │ │ │ │ │ │ └── progress-list.component.css 137 | │ │ │ │ ├── services/ 138 | │ │ │ │ │ ├── auth.interceptor.ts 139 | │ │ │ │ │ ├── auth.service.ts 140 | │ │ │ │ │ ├── user.service.ts 141 | │ │ │ │ │ ├── course.service.ts 142 | │ │ │ │ │ ├── lesson.service.ts 143 | │ │ │ │ │ ├── enrollment.service.ts 144 | │ │ │ │ │ └── progress.service.ts 145 | │ │ │ │ ├── app.routes.ts 146 | │ │ │ │ ├── app.component.ts 147 | │ │ │ │ ├── app.config.ts 148 | │ │ │ │ ├── app.config.service.ts 149 | │ │ │ │ ├── app.component.html 150 | │ │ │ │ └── app.component.css 151 | │ │ │ ├── assets/ 152 | │ │ │ │ └── images/ 153 | │ │ │ │ └── .gitkeep 154 | │ │ │ ├── main.ts 155 | │ │ │ ├── styles.css 156 | │ │ │ └── index.html 157 | │ │ ├── .editorconfig 158 | │ │ ├── .gitignore 159 | │ │ ├── angular.json 160 | │ │ ├── package.json 161 | │ │ ├── package-lock.json 162 | │ │ ├── tsconfig.json 163 | │ │ ├── tsconfig.app.json 164 | │ │ └── tsconfig.spec.json 165 | │ ├── LICENSE 166 | │ ├── README.md 167 | ├── .gitignore 168 | ├── LICENSE 169 | └── README.md 170 | ``` 171 | 172 | ## Getting Started 173 | 174 | ### Prerequisites 175 | 176 | Make sure you have the following installed on your machine: 177 | 178 | - **Node.js** (v18.19 or later) 179 | - **Angular CLI** (v12 or later) 180 | - **Python** (v3.7 or later) 181 | 182 | To check if you have these installed, run: 183 | 184 | ```bash 185 | node -v 186 | npm -v 187 | ng version 188 | python --version 189 | ``` 190 | 191 | ### Backend Setup 192 | 193 | It is of utmost importance to set up the backend server before running the frontend application. 194 | 195 | 1. **Navigate to the backend directory:** 196 | 197 | ```bash 198 | cd Learning-Management-System/LMS-Backend 199 | ``` 200 | 201 | 2. **Create a virtual environment:** 202 | 203 | ```bash 204 | python -m venv venv 205 | source venv/bin/activate # On Windows, use `venv\Scripts\activate` 206 | ``` 207 | 208 | 3. **Install dependencies:** 209 | 210 | ```bash 211 | pip install -r requirements.txt 212 | ``` 213 | 214 | 4. **Run the backend server:** 215 | 216 | ```bash 217 | python manage.py runserver 218 | ``` 219 | 220 | ### Frontend Setup 221 | 222 | 1. **Navigate to the frontend directory:** 223 | 224 | ```bash 225 | cd Learning-Management-System/LMS-Frontend/app 226 | ``` 227 | 228 | 2. **Install dependencies:** 229 | 230 | ```bash 231 | npm install 232 | ``` 233 | 234 | 3. **Start the development server:** 235 | 236 | ```bash 237 | ng serve 238 | ``` 239 | 240 | 4. **Open your browser and navigate to:** 241 | 242 | ``` 243 | http://localhost:4200 244 | ``` 245 | 246 | ## Styling and Design 247 | 248 | - **Framework:** Bootstrap is used for responsive design. 249 | - **Custom CSS:** Additional styles are added in the component-specific CSS files. 250 | - **Charting:** Chart.js is used for visualizing data in various components. 251 | - **Icons:** Icons are sourced from Font Awesome. 252 | - **Colors:** The color scheme is kept minimalistic with shades of blue and white. 253 | - **Typography:** The default font is set to Google Poppins. 254 | - **Layout:** The layout is designed to be user-friendly and intuitive. 255 | - **Responsive Design:** The application is optimized for various screen sizes. 256 | - **Animations:** Angular animations are used for transitions and effects. 257 | 258 | ## Troubleshooting 259 | 260 | ### Common Issues 261 | 262 | 1. **CORS Errors:** 263 | - Ensure your backend server is configured to accept requests from `http://localhost:4200`. This should have been set up already in the Django backend. 264 | 265 | 2. **Unauthorized Access Errors:** 266 | - Make sure the token is correctly stored in `localStorage` after login and is sent with each request that requires authorization. 267 | 268 | 3. **API Connection Issues:** 269 | - Verify that the backend server is running and accessible at `http://127.0.0.1:8000`. 270 | 271 | ### Debugging Tips 272 | 273 | - Use the browser's Developer Tools (F12) to inspect requests, responses, and console logs. 274 | - Check the network tab for failed requests and see the status codes and messages. 275 | 276 | ## Contributing 277 | 278 | Feel free to submit issues and contribute to the development of this project by opening a pull request. 279 | 280 | ## License 281 | 282 | This project is licensed under the [MIT License](LICENSE). 283 | 284 | ## Contact 285 | 286 | If you have any questions or need further assistance, feel free to contact me at [hoangson091104@gmail.com](mailto:hoangson091104@gmail.com). 287 | 288 | --- 289 | 290 | **[⬆ Back to Top](#e-learning-management-system-frontend)** 291 | -------------------------------------------------------------------------------- /LMS-Frontend/app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /LMS-Frontend/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /LMS-Frontend/app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /LMS-Frontend/app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /LMS-Frontend/app/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LMS-Frontend/app/README.md: -------------------------------------------------------------------------------- 1 | # E-Learning Management System Frontend 2 | 3 | The **E-Learning Management System Frontend** is the frontend component of the E-Learning Management System, a web-based application that allows administrators and users to interact with the e-learning platform's various components. The frontend is developed with Angular, and the backend is built using Django REST Framework. 4 | 5 | ## Table of Contents 6 | 7 | - [Project Overview](#project-overview) 8 | - [Features](#features) 9 | - [User Interfaces](#user-interfaces) 10 | - [File Structure](#file-structure) 11 | - [Getting Started](#getting-started) 12 | - [Prerequisites](#prerequisites) 13 | - [Backend Setup (Django)](#backend-setup) 14 | - [Frontend Setup (Angular)](#frontend-setup) 15 | - [Styling and Design](#styling-and-design) 16 | - [Troubleshooting](#troubleshooting) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | - [Contact](#contact) 20 | 21 | ## Project Overview 22 | 23 | This frontend project is built with Angular, a popular web application framework developed by Google. It provides a user-friendly interface for managing courses, lessons, users, enrollments, and progress tracking. The frontend interacts with the backend APIs defined by Django to fetch and update data, handle user authentication, and display dynamic content. 24 | 25 | ## Features 26 | 27 | - **User Authentication**: Token-based login system. 28 | - **Course Management**: Display, create, update, and delete courses. 29 | - **Lesson Management**: Display, create, update, and delete lessons. 30 | - **User Management**: List and manage users. 31 | - **Enrollment Management**: Handle course enrollments. 32 | - **Progress Tracking**: Monitor user progress across lessons. 33 | - **Responsive Design**: Optimized for various devices. 34 | - **Charts and Data Visualization**: Implemented with Chart.js for dynamic data visualization. 35 | 36 | ## User Interfaces 37 | 38 | **Home Page**: 39 | 40 |

41 | Home Page 42 |

43 | 44 | **Course List**: 45 | 46 |

47 | Course List 48 |

49 | 50 | **Lesson List**: 51 | 52 |

53 | Lesson List 54 |

55 | 56 | **User List**: 57 | 58 |

59 | User List 60 |

61 | 62 | **Enrollment List**: 63 | 64 |

65 | Enrollment List 66 |

67 | 68 | **Progress List**: 69 | 70 |

71 | Progress List 72 |

73 | 74 | **Login Page**: 75 | 76 |

77 | Login Page 78 |

79 | 80 | ## File Structure 81 | 82 | The repository is organized as follows: 83 | 84 | ```plaintext 85 | Learning-Management-System/ 86 | ├── LMS-Backend 87 | │ ├── (Backend code) 88 | │ └── ... 89 | ├── LMS-Frontend 90 | │ ├── angular.json 91 | │ ├── package.json 92 | │ ├── README.md 93 | │ ├── LICENSE 94 | │ ├── app/ 95 | │ │ ├── src/ 96 | │ │ │ ├── app/ 97 | │ │ │ │ ├── auth/ 98 | │ │ │ │ │ ├── login/ 99 | │ │ │ │ │ │ ├── login.component.ts 100 | │ │ │ │ │ │ ├── login.component.html 101 | │ │ │ │ │ │ └── login.component.css 102 | │ │ │ │ ├── core/ 103 | │ │ │ │ │ ├── footer/ 104 | │ │ │ │ │ │ ├── footer.component.ts 105 | │ │ │ │ │ │ ├── footer.component.html 106 | │ │ │ │ │ │ └── footer.component.css 107 | │ │ │ │ │ ├── header/ 108 | │ │ │ │ │ │ ├── header.component.ts 109 | │ │ │ │ │ │ ├── header.component.html 110 | │ │ │ │ │ │ └── header.component.css 111 | │ │ │ │ ├── pages/ 112 | │ │ │ │ │ ├── home/ 113 | │ │ │ │ │ │ ├── home.component.ts 114 | │ │ │ │ │ │ ├── home.component.html 115 | │ │ │ │ │ │ └── home.component.css 116 | │ │ │ │ ├── components/ 117 | │ │ │ │ │ ├── course-list/ 118 | │ │ │ │ │ │ ├── course-list.component.ts 119 | │ │ │ │ │ │ ├── course-list.component.html 120 | │ │ │ │ │ │ └── course-list.component.css 121 | │ │ │ │ │ ├── lesson-list/ 122 | │ │ │ │ │ │ ├── lesson-list.component.ts 123 | │ │ │ │ │ │ ├── lesson-list.component.html 124 | │ │ │ │ │ │ └── lesson-list.component.css 125 | │ │ │ │ │ ├── user-list/ 126 | │ │ │ │ │ │ ├── user-list.component.ts 127 | │ │ │ │ │ │ ├── user-list.component.html 128 | │ │ │ │ │ │ └── user-list.component.css 129 | │ │ │ │ │ ├── enrollment-list/ 130 | │ │ │ │ │ │ ├── enrollment-list.component.ts 131 | │ │ │ │ │ │ ├── enrollment-list.component.html 132 | │ │ │ │ │ │ └── enrollment-list.component.css 133 | │ │ │ │ │ ├── progress-list/ 134 | │ │ │ │ │ │ ├── progress-list.component.ts 135 | │ │ │ │ │ │ ├── progress-list.component.html 136 | │ │ │ │ │ │ └── progress-list.component.css 137 | │ │ │ │ ├── services/ 138 | │ │ │ │ │ ├── auth.interceptor.ts 139 | │ │ │ │ │ ├── auth.service.ts 140 | │ │ │ │ │ ├── user.service.ts 141 | │ │ │ │ │ ├── course.service.ts 142 | │ │ │ │ │ ├── lesson.service.ts 143 | │ │ │ │ │ ├── enrollment.service.ts 144 | │ │ │ │ │ └── progress.service.ts 145 | │ │ │ │ ├── app.routes.ts 146 | │ │ │ │ ├── app.component.ts 147 | │ │ │ │ ├── app.config.ts 148 | │ │ │ │ ├── app.config.service.ts 149 | │ │ │ │ ├── app.component.html 150 | │ │ │ │ └── app.component.css 151 | │ │ │ ├── assets/ 152 | │ │ │ │ └── images/ 153 | │ │ │ │ └── .gitkeep 154 | │ │ │ ├── main.ts 155 | │ │ │ ├── styles.css 156 | │ │ │ └── index.html 157 | │ │ ├── .editorconfig 158 | │ │ ├── .gitignore 159 | │ │ ├── angular.json 160 | │ │ ├── package.json 161 | │ │ ├── package-lock.json 162 | │ │ ├── tsconfig.json 163 | │ │ ├── tsconfig.app.json 164 | │ │ └── tsconfig.spec.json 165 | │ ├── LICENSE 166 | │ ├── README.md 167 | ├── .gitignore 168 | ├── LICENSE 169 | └── README.md 170 | ``` 171 | 172 | ## Getting Started 173 | 174 | ### Prerequisites 175 | 176 | Make sure you have the following installed on your machine: 177 | 178 | - **Node.js** (v18.19 or later) 179 | - **Angular CLI** (v12 or later) 180 | - **Python** (v3.7 or later) 181 | 182 | To check if you have these installed, run: 183 | 184 | ```bash 185 | node -v 186 | npm -v 187 | ng version 188 | python --version 189 | ``` 190 | 191 | ### Backend Setup 192 | 193 | It is of utmost importance to set up the backend server before running the frontend application. 194 | 195 | 1. **Navigate to the backend directory:** 196 | 197 | ```bash 198 | cd Learning-Management-System/LMS-Backend 199 | ``` 200 | 201 | 2. **Create a virtual environment:** 202 | 203 | ```bash 204 | python -m venv venv 205 | source venv/bin/activate # On Windows, use `venv\Scripts\activate` 206 | ``` 207 | 208 | 3. **Install dependencies:** 209 | 210 | ```bash 211 | pip install -r requirements.txt 212 | ``` 213 | 214 | 4. **Run the backend server:** 215 | 216 | ```bash 217 | python manage.py runserver 218 | ``` 219 | 220 | ### Frontend Setup 221 | 222 | 1. **Navigate to the frontend directory:** 223 | 224 | ```bash 225 | cd Learning-Management-System/LMS-Frontend/app 226 | ``` 227 | 228 | 2. **Install dependencies:** 229 | 230 | ```bash 231 | npm install 232 | ``` 233 | 234 | 3. **Start the development server:** 235 | 236 | ```bash 237 | ng serve 238 | ``` 239 | 240 | 4. **Open your browser and navigate to:** 241 | 242 | ``` 243 | http://localhost:4200 244 | ``` 245 | 246 | ## Styling and Design 247 | 248 | - **Framework:** Bootstrap is used for responsive design. 249 | - **Custom CSS:** Additional styles are added in the component-specific CSS files. 250 | - **Charting:** Chart.js is used for visualizing data in various components. 251 | - **Icons:** Icons are sourced from Font Awesome. 252 | - **Colors:** The color scheme is kept minimalistic with shades of blue and white. 253 | - **Typography:** The default font is set to Google Poppins. 254 | - **Layout:** The layout is designed to be user-friendly and intuitive. 255 | - **Responsive Design:** The application is optimized for various screen sizes. 256 | - **Animations:** Angular animations are used for transitions and effects. 257 | 258 | ## Troubleshooting 259 | 260 | ### Common Issues 261 | 262 | 1. **CORS Errors:** 263 | - Ensure your backend server is configured to accept requests from `http://localhost:4200`. This should have been set up already in the Django backend. 264 | 265 | 2. **Unauthorized Access Errors:** 266 | - Make sure the token is correctly stored in `localStorage` after login and is sent with each request that requires authorization. 267 | 268 | 3. **API Connection Issues:** 269 | - Verify that the backend server is running and accessible at `http://127.0.0.1:8000`. 270 | 271 | ### Debugging Tips 272 | 273 | - Use the browser's Developer Tools (F12) to inspect requests, responses, and console logs. 274 | - Check the network tab for failed requests and see the status codes and messages. 275 | 276 | ## Contributing 277 | 278 | Feel free to submit issues and contribute to the development of this project by opening a pull request. 279 | 280 | ## License 281 | 282 | This project is licensed under the [MIT License](LICENSE). 283 | 284 | ## Contact 285 | 286 | If you have any questions or need further assistance, feel free to contact me at [hoangson091104@gmail.com](mailto:hoangson091104@gmail.com). 287 | 288 | --- 289 | 290 | **[⬆ Back to Top](#e-learning-management-system-frontend)** 291 | -------------------------------------------------------------------------------- /LMS-Frontend/app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": true 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": true 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/app", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": [ 30 | "zone.js" 31 | ], 32 | "tsConfig": "tsconfig.app.json", 33 | "assets": [ 34 | "src/favicon.ico", 35 | "src/manifest.json", 36 | "src/assets" 37 | ], 38 | "styles": [ 39 | "src/styles.css", 40 | "node_modules/bootstrap/dist/css/bootstrap.min.css" 41 | ], 42 | "scripts": [ 43 | "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" 44 | ] 45 | }, 46 | "configurations": { 47 | "production": { 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "500kb", 52 | "maximumError": "1mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "2kb", 57 | "maximumError": "4kb" 58 | } 59 | ], 60 | "outputHashing": "all" 61 | }, 62 | "development": { 63 | "buildOptimizer": false, 64 | "optimization": false, 65 | "vendorChunk": true, 66 | "extractLicenses": false, 67 | "sourceMap": true, 68 | "namedChunks": true 69 | } 70 | }, 71 | "defaultConfiguration": "production" 72 | }, 73 | "serve": { 74 | "builder": "@angular-devkit/build-angular:dev-server", 75 | "configurations": { 76 | "production": { 77 | "browserTarget": "app:build:production" 78 | }, 79 | "development": { 80 | "browserTarget": "app:build:development" 81 | } 82 | }, 83 | "defaultConfiguration": "development" 84 | }, 85 | "extract-i18n": { 86 | "builder": "@angular-devkit/build-angular:extract-i18n", 87 | "options": { 88 | "browserTarget": "app:build" 89 | } 90 | }, 91 | "test": { 92 | "builder": "@angular-devkit/build-angular:karma", 93 | "options": { 94 | "polyfills": [ 95 | "zone.js", 96 | "zone.js/testing" 97 | ], 98 | "tsConfig": "tsconfig.spec.json", 99 | "assets": [ 100 | "src/favicon.ico", 101 | "src/assets" 102 | ], 103 | "styles": [ 104 | "src/styles.css" 105 | ], 106 | "scripts": [] 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "cli": { 113 | "analytics": false 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /LMS-Frontend/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elearning-frontend", 3 | "displayName": "E-Learning Management System Frontend", 4 | "description": "This is the frontend of the E-Learning Management System", 5 | "author": "Son Nguyen", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/hoangsonww/Learning-Management-System.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/hoangsonww/Learning-Management-System/issues" 13 | }, 14 | "version": "1.1.0", 15 | "scripts": { 16 | "ng": "ng", 17 | "start": "ng serve", 18 | "build": "ng build", 19 | "watch": "ng build --watch --configuration development", 20 | "test": "ng test" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular/animations": "^16.0.0", 25 | "@angular/common": "^16.0.0", 26 | "@angular/compiler": "^16.0.0", 27 | "@angular/core": "^16.0.0", 28 | "@angular/forms": "^16.0.0", 29 | "@angular/platform-browser": "^16.0.0", 30 | "@angular/platform-browser-dynamic": "^16.0.0", 31 | "@angular/router": "^16.0.0", 32 | "bootstrap": "^5.3.3", 33 | "chart.js": "^4.4.4", 34 | "rxjs": "~7.8.0", 35 | "tslib": "^2.3.0", 36 | "zone.js": "~0.13.0" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^16.0.4", 40 | "@angular/cli": "~16.0.4", 41 | "@angular/compiler-cli": "^16.0.0", 42 | "@types/jasmine": "~4.3.0", 43 | "jasmine-core": "~4.6.0", 44 | "karma": "~6.4.0", 45 | "karma-chrome-launcher": "~3.2.0", 46 | "karma-coverage": "~2.2.0", 47 | "karma-jasmine": "~5.1.0", 48 | "karma-jasmine-html-reporter": "~2.0.0", 49 | "typescript": "~5.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/android-chrome-192x192.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/android-chrome-512x512.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { HeaderComponent } from './core/header/header.component'; 4 | import { FooterComponent } from './core/footer/footer.component'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | imports: [RouterModule, HeaderComponent, FooterComponent], 10 | template: ` 11 | 12 |
13 | 14 |
15 | 16 | `, 17 | styles: [ 18 | ` 19 | .container { 20 | margin-top: 20px; 21 | padding: 15px; 22 | max-width: 1200px; 23 | margin-left: auto; 24 | margin-right: auto; 25 | } 26 | `, 27 | ], 28 | }) 29 | export class AppComponent {} 30 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideHttpClient, HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { AuthInterceptor } from './services/auth.interceptor'; 4 | 5 | export const config: ApplicationConfig = { 6 | providers: [ 7 | provideHttpClient(), 8 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 9 | importProvidersFrom([]), 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { provideHttpClient, withFetch } from '@angular/common/http'; 4 | import { routes } from './app.routes'; 5 | import { provideClientHydration } from '@angular/platform-browser'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [ 9 | provideRouter(routes), 10 | provideHttpClient(withFetch()), 11 | provideClientHydration(), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { HomeComponent } from './pages/home/home.component'; 3 | import { LoginComponent } from './auth/login/login.component'; 4 | import { UserListComponent } from './components/user-list/user-list.component'; 5 | import { CourseListComponent } from './components/course-list/course-list.component'; 6 | import { LessonListComponent } from './components/lesson-list/lesson-list.component'; 7 | import { EnrollmentListComponent } from './components/enrollment-list/enrollment-list.component'; 8 | import { ProgressListComponent } from './components/progress-list/progress-list.component'; 9 | import { RegisterComponent } from './auth/register/register.component'; 10 | import { NotFoundComponent } from './pages/notfound/notfound.component'; 11 | 12 | export const routes: Routes = [ 13 | { path: '', component: HomeComponent }, 14 | { path: 'login', component: LoginComponent }, 15 | { path: 'register', component: RegisterComponent }, 16 | { path: 'users', component: UserListComponent }, 17 | { path: 'courses', component: CourseListComponent }, 18 | { path: 'lessons', component: LessonListComponent }, 19 | { path: 'enrollments', component: EnrollmentListComponent }, 20 | { path: 'progress', component: ProgressListComponent }, 21 | { path: '**', component: NotFoundComponent }, 22 | ]; 23 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/login/login.component.css: -------------------------------------------------------------------------------- 1 | .login-container { 2 | min-height: 80vh; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | margin-top: 0; 7 | } 8 | 9 | .form-label { 10 | font-size: 1.1rem; 11 | font-weight: 500; 12 | margin-bottom: 0.5rem; 13 | color: #333; 14 | margin-right: 10px; 15 | } 16 | 17 | .form-control-lg { 18 | height: calc(2rem + 2px); 19 | padding: 0.75rem 1rem; 20 | border-radius: 8px; 21 | border: 1px solid #ccc; 22 | transition: border-color 0.3s ease; 23 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 24 | margin-top: 5px; 25 | font: inherit; 26 | } 27 | 28 | .form-control-lg:focus { 29 | border-color: #007bff; 30 | box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); 31 | } 32 | 33 | .login-card { 34 | max-width: 400px; 35 | width: 100%; 36 | border-radius: 12px; 37 | border: none; 38 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); 39 | padding: 20px; 40 | } 41 | 42 | .btn-primary { 43 | font-weight: 600; 44 | padding: 0.75rem 1rem; 45 | font-size: 1rem; 46 | transition: 47 | background-color 0.3s, 48 | transform 0.3s; 49 | border-radius: 8px; 50 | } 51 | 52 | .btn-primary:hover { 53 | background-color: #0056b3; 54 | transform: scale(1.05); 55 | } 56 | 57 | .alert-danger { 58 | font-size: 0.95rem; 59 | padding: 12px; 60 | border-radius: 8px; 61 | } 62 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 | 7 | 16 |
17 |
18 | 19 | 28 |
29 | 30 | 31 |
32 | 48 |
49 | 50 | 51 |
52 | {{ errorMessage }} 53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from '../../services/auth.service'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | standalone: true, 10 | imports: [FormsModule, CommonModule], 11 | templateUrl: './login.component.html', 12 | styleUrls: ['./login.component.css'], 13 | }) 14 | export class LoginComponent { 15 | username: string = ''; 16 | password: string = ''; 17 | errorMessage: string = ''; 18 | isLoading: boolean = false; // New flag to manage loading state 19 | 20 | constructor( 21 | private authService: AuthService, 22 | private router: Router, 23 | ) {} 24 | 25 | login(): void { 26 | // Reset error message before login attempt 27 | this.errorMessage = ''; 28 | 29 | // Set loading state to true 30 | this.isLoading = true; 31 | 32 | this.authService.login(this.username, this.password).subscribe( 33 | () => { 34 | // Reset loading state on success 35 | this.isLoading = false; 36 | this.router.navigate(['/']); // Redirect to home page after successful login 37 | }, 38 | (error) => { 39 | // Reset loading state and show error on failure 40 | this.isLoading = false; 41 | console.log(error); 42 | this.errorMessage = 43 | error.error?.detail || 44 | 'Invalid username or password. Please try registering first.'; 45 | }, 46 | ); 47 | } 48 | 49 | logout(): void { 50 | this.authService.logout(); 51 | this.router.navigate(['/login']); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/register/register.component.css: -------------------------------------------------------------------------------- 1 | .register-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin-top: 0; 6 | } 7 | 8 | .form-label { 9 | font-size: 1.1rem; 10 | font-weight: 500; 11 | margin-bottom: 0.5rem; 12 | color: #333; 13 | margin-right: 10px; 14 | } 15 | 16 | .form-control-lg { 17 | height: calc(2rem + 2px); 18 | padding: 0.75rem 1rem; 19 | border-radius: 8px; 20 | border: 1px solid #ccc; 21 | transition: border-color 0.3s ease; 22 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 23 | margin-top: 5px; 24 | font: inherit; 25 | } 26 | 27 | .form-control-lg:focus { 28 | border-color: #007bff; 29 | box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); 30 | } 31 | 32 | .register-card { 33 | max-width: 400px; 34 | width: 100%; 35 | border-radius: 12px; 36 | border: none; 37 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); 38 | padding: 20px; 39 | } 40 | 41 | .btn-primary { 42 | font-weight: 600; 43 | padding: 0.75rem 1rem; 44 | font-size: 1rem; 45 | transition: 46 | background-color 0.3s, 47 | transform 0.3s; 48 | border-radius: 8px; 49 | } 50 | 51 | .btn-primary:hover { 52 | background-color: #0056b3; 53 | transform: scale(1.05); 54 | } 55 | 56 | .alert-danger { 57 | font-size: 0.95rem; 58 | padding: 12px; 59 | border-radius: 8px; 60 | } 61 | 62 | a { 63 | color: #007bff !important; 64 | text-decoration: none; 65 | cursor: pointer; 66 | } 67 | 68 | a:hover { 69 | text-decoration: underline !important; 70 | } 71 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/register/register.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |

Register

6 |
7 | 8 |
9 | 10 | 20 |
24 | Username is required. 25 |
26 |
27 | 28 | 29 |
30 | 31 | 42 |
43 | Email is required. 44 | Enter a valid email address. 47 |
48 |
49 | 50 | 51 |
52 | 53 | 64 |
68 | Password is required. 69 |
70 |
71 | {{ passwordError }} 72 |
73 |
74 | 75 | 76 |
77 | 78 | 88 |
92 | Passwords do not match. 93 |
94 |
95 | 96 | 97 |
98 | 114 |
115 | 116 | 117 |
118 | {{ errorMessage }} 119 |
120 | 121 |
122 | Already have an account? Login 123 |
124 |
125 |
126 |
127 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/auth/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from '../../services/auth.service'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | @Component({ 8 | selector: 'app-register', 9 | standalone: true, 10 | imports: [FormsModule, CommonModule], 11 | templateUrl: './register.component.html', 12 | styleUrls: ['./register.component.css'], 13 | }) 14 | export class RegisterComponent { 15 | username: string = ''; 16 | email: string = ''; 17 | password1: string = ''; 18 | password2: string = ''; 19 | errorMessage: string = ''; 20 | passwordError: string | null = null; 21 | isLoading: boolean = false; // New flag to manage loading state 22 | 23 | constructor( 24 | private authService: AuthService, 25 | private router: Router, 26 | ) {} 27 | 28 | /** 29 | * This function ensures the password is not purely numeric and also 30 | * provides error messages for form validation. 31 | */ 32 | validatePassword(): void { 33 | const isNumeric = /^\d+$/.test(this.password1); 34 | if (isNumeric) { 35 | this.passwordError = 'Password cannot be entirely numeric.'; 36 | } else { 37 | this.passwordError = null; // Reset if valid 38 | } 39 | } 40 | 41 | goToLogin(): void { 42 | this.router.navigate(['/login']); 43 | } 44 | 45 | register(): void { 46 | this.errorMessage = ''; 47 | 48 | // Validate password match 49 | if (this.password1 !== this.password2) { 50 | this.errorMessage = 'Passwords do not match.'; 51 | return; 52 | } 53 | 54 | if (this.passwordError) { 55 | return; 56 | } 57 | 58 | // Set loading state to true before making the request 59 | this.isLoading = true; 60 | 61 | this.authService 62 | .register(this.username, this.email, this.password1, this.password2) 63 | .subscribe( 64 | () => { 65 | // Reset loading state on success 66 | this.isLoading = false; 67 | this.router.navigate(['/login']); // Redirect to login page after successful registration 68 | }, 69 | (error) => { 70 | // Reset loading state on failure 71 | this.isLoading = false; 72 | 73 | // Handle and display specific error messages from the backend 74 | if (error.error) { 75 | if (error.error.username) { 76 | this.errorMessage = error.error.username[0]; // Display the first error message related to username 77 | } else if (error.error.email) { 78 | this.errorMessage = error.error.email[0]; // Display the first error message related to email 79 | } else if (error.error.password1) { 80 | this.errorMessage = error.error.password1[0]; // Display the first error message related to password1 81 | } else if (error.error.non_field_errors) { 82 | this.errorMessage = error.error.non_field_errors[0]; // Display general error messages 83 | } else { 84 | this.errorMessage = 'Registration failed. Please try again.'; 85 | } 86 | } else { 87 | this.errorMessage = 'An unknown error occurred. Please try again.'; 88 | } 89 | }, 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/course-list/course-list.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1200px; 3 | margin: auto; 4 | } 5 | 6 | .chart-section { 7 | margin-bottom: 20px; 8 | display: flex; 9 | justify-content: center; 10 | margin-top: 20px; 11 | } 12 | 13 | .chart-container { 14 | width: 300px; 15 | height: 300px; 16 | } 17 | 18 | .card { 19 | border: none; 20 | transition: 21 | transform 0.3s, 22 | box-shadow 0.3s; 23 | padding: 15px; 24 | } 25 | 26 | .card:hover { 27 | transform: translateY(-5px); 28 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .card-title { 32 | font-size: 1.5rem; 33 | font-weight: 700; 34 | margin-bottom: 10px; 35 | } 36 | 37 | .card-text { 38 | color: #666; 39 | font-size: 0.95rem; 40 | } 41 | 42 | .rounded-custom { 43 | border-radius: 15px; 44 | margin: 15px; 45 | } 46 | 47 | .loading-overlay { 48 | position: fixed; /* Cover the entire viewport */ 49 | top: 0; 50 | left: 0; 51 | width: 100vw; 52 | height: 100vh; 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | background-color: rgba( 57 | 255, 58 | 255, 59 | 255, 60 | 0.8 61 | ); /* Optional: light overlay effect */ 62 | z-index: 9999; /* Ensure it stays on top */ 63 | } 64 | 65 | .spinner-border { 66 | width: 3rem; 67 | height: 3rem; 68 | } 69 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/course-list/course-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Platform Overview

3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | Loading... 15 |
16 |
17 | 18 | 19 |
20 |

Courses

21 |
26 | {{ errorMessage }} 27 |
28 |
29 | No courses available. 30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |

{{ course.title }}

39 |

{{ course.description }}

40 |
41 |

42 | Instructor: 43 | {{ formatInstructorName(course.instructor) }} 44 |

45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/course-list/course-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CourseService } from '../../services/course.service'; 3 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 4 | import { CommonModule } from '@angular/common'; 5 | import { Chart, ChartConfiguration, registerables } from 'chart.js'; 6 | import { forkJoin } from 'rxjs'; 7 | 8 | Chart.register(...registerables); 9 | 10 | @Component({ 11 | selector: 'app-course-list', 12 | standalone: true, 13 | imports: [CommonModule], 14 | templateUrl: './course-list.component.html', 15 | styleUrls: ['./course-list.component.css'], 16 | }) 17 | export class CourseListComponent implements OnInit { 18 | courses: any[] = []; 19 | errorMessage: string = ''; 20 | loading: boolean = true; // Track loading state 21 | chart: Chart<'pie'> | undefined; 22 | private apiUrl = 23 | 'https://learning-management-system-fullstack.onrender.com/api/'; 24 | lessonsLength: number = 0; 25 | enrollmentsLength: number = 0; 26 | isAuthenticated: boolean = true; // Flag to check authentication status 27 | 28 | constructor( 29 | private courseService: CourseService, 30 | private http: HttpClient, 31 | ) {} 32 | 33 | ngOnInit(): void { 34 | this.fetchAllData(); 35 | } 36 | 37 | fetchAllData(): void { 38 | const token = localStorage.getItem('authToken'); 39 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 40 | 41 | const courses$ = this.courseService.getCourses(); 42 | const lessons$ = this.http.get(`${this.apiUrl}lessons/`, { headers }); 43 | const enrollments$ = this.http.get(`${this.apiUrl}enrollments/`, { 44 | headers, 45 | }); 46 | 47 | forkJoin([courses$, lessons$, enrollments$]).subscribe( 48 | ([coursesData, lessonsData, enrollmentsData]: [any[], any, any]) => { 49 | this.courses = coursesData; 50 | this.lessonsLength = lessonsData.length; 51 | this.enrollmentsLength = enrollmentsData.length; 52 | this.loading = false; // Stop loading after data fetching is complete 53 | if (this.isAuthenticated) { 54 | this.renderChart(); // Render the chart after data is fetched 55 | } 56 | }, 57 | (error) => { 58 | this.loading = false; // Stop loading in case of error 59 | if (error.status === 401) { 60 | this.isAuthenticated = false; // Set flag to false on unauthorized access 61 | this.errorMessage = 'Unauthorized access. Please log in.'; 62 | } else { 63 | this.errorMessage = 64 | 'Error fetching data due to expired token. Please try registering and logging in again.'; 65 | } 66 | this.lessonsLength = 20; 67 | this.enrollmentsLength = 50; 68 | if (this.isAuthenticated) { 69 | this.renderChart(); // Ensure the chart is only rendered if authenticated 70 | } 71 | }, 72 | ); 73 | } 74 | 75 | formatInstructorName(instructor: any): string { 76 | return 'John Doe'; 77 | } 78 | 79 | renderChart(): void { 80 | const ctx = document.getElementById('courseChart') as HTMLCanvasElement; 81 | 82 | if (this.chart) { 83 | this.chart.destroy(); // Destroy any existing chart instance before creating a new one 84 | } 85 | 86 | const chartConfig: ChartConfiguration<'pie'> = { 87 | type: 'pie', 88 | data: { 89 | labels: ['Courses', 'Lessons', 'Enrollments'], 90 | datasets: [ 91 | { 92 | data: [ 93 | this.courses.length, 94 | this.lessonsLength, 95 | this.enrollmentsLength, 96 | ], 97 | backgroundColor: ['#007bff', '#ffc107', '#28a745'], 98 | }, 99 | ], 100 | }, 101 | options: { 102 | responsive: true, 103 | plugins: { 104 | legend: { 105 | position: 'bottom', 106 | }, 107 | }, 108 | }, 109 | }; 110 | 111 | this.chart = new Chart(ctx, chartConfig); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/enrollment-list/enrollment-list.component.css: -------------------------------------------------------------------------------- 1 | .chart-section { 2 | margin-bottom: 20px; 3 | display: flex; 4 | justify-content: center; 5 | margin-top: 20px; 6 | } 7 | 8 | .chart-container { 9 | width: 300px; 10 | height: 300px; 11 | } 12 | 13 | .container { 14 | padding-top: 0; 15 | } 16 | 17 | .card { 18 | border: none; 19 | transition: 20 | transform 0.3s, 21 | box-shadow 0.3s; 22 | padding: 15px; 23 | } 24 | 25 | .card:hover { 26 | transform: translateY(-5px); 27 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); 28 | } 29 | 30 | .card-title { 31 | font-size: 1.5rem; 32 | font-weight: 700; 33 | margin-bottom: 10px; 34 | } 35 | 36 | .card-text { 37 | color: #666; 38 | font-size: 0.95rem; 39 | } 40 | 41 | .rounded-custom { 42 | border-radius: 15px; 43 | margin: 15px; 44 | } 45 | 46 | .loading-overlay { 47 | position: fixed; /* Cover the entire viewport */ 48 | top: 0; 49 | left: 0; 50 | width: 100vw; 51 | height: 100vh; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | background-color: rgba( 56 | 255, 57 | 255, 58 | 255, 59 | 0.8 60 | ); /* Optional: light overlay effect */ 61 | z-index: 9999; /* Ensure it stays on top */ 62 | } 63 | 64 | .spinner-border { 65 | width: 3rem; 66 | height: 3rem; 67 | } 68 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/enrollment-list/enrollment-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Platform Overview

3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | Loading... 15 |
16 |
17 | 18 | 19 |
20 |

Enrollment List

21 |
22 | {{ errorMessage }} 23 |
24 |
28 | No enrollments available. 29 |
30 | 31 | 32 |
33 |
37 |
38 |
39 |
40 |

41 | User: {{ enrollment.student }} 42 |

43 |

44 | Course: {{ enrollment.course }} 45 |

46 | 47 |
48 |

49 | Date Enrolled: 50 | {{ enrollment.enrolled_at | date: "fullDate" }} 51 |

52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/enrollment-list/enrollment-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { EnrollmentService } from '../../services/enrollment.service'; 3 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 4 | import { CommonModule } from '@angular/common'; 5 | import { Chart, ChartConfiguration, registerables } from 'chart.js'; 6 | import { forkJoin } from 'rxjs'; 7 | 8 | interface Enrollment { 9 | _id: string; 10 | student: string; 11 | course: string; 12 | enrolled_at: string; 13 | } 14 | 15 | Chart.register(...registerables); 16 | 17 | @Component({ 18 | selector: 'app-enrollment-list', 19 | standalone: true, 20 | imports: [CommonModule], 21 | templateUrl: './enrollment-list.component.html', 22 | styleUrls: ['./enrollment-list.component.css'], 23 | }) 24 | export class EnrollmentListComponent implements OnInit { 25 | enrollments: Enrollment[] = []; 26 | errorMessage: string = ''; 27 | loading: boolean = true; // Track loading state 28 | chart: Chart<'pie'> | undefined; 29 | private apiUrl = 30 | 'https://learning-management-system-fullstack.onrender.com/api/'; 31 | coursesLength: number = 0; 32 | lessonsLength: number = 0; 33 | isAuthenticated: boolean = true; // Flag to check authentication status 34 | 35 | constructor( 36 | private enrollmentService: EnrollmentService, 37 | private http: HttpClient, 38 | ) {} 39 | 40 | ngOnInit(): void { 41 | this.fetchAllData(); 42 | } 43 | 44 | fetchAllData(): void { 45 | const token = localStorage.getItem('authToken'); 46 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 47 | 48 | const enrollments$ = this.enrollmentService.getEnrollments(); 49 | const courses$ = this.http.get(`${this.apiUrl}courses/`, { headers }); 50 | const lessons$ = this.http.get(`${this.apiUrl}lessons/`, { headers }); 51 | 52 | forkJoin([enrollments$, courses$, lessons$]).subscribe( 53 | ([enrollmentsData, coursesData, lessonsData]: [ 54 | Enrollment[], 55 | any, 56 | any, 57 | ]) => { 58 | this.enrollments = enrollmentsData; 59 | this.coursesLength = coursesData.length; 60 | this.lessonsLength = lessonsData.length; 61 | this.fetchUserDetails(); // Fetch user details based on student ID 62 | }, 63 | (error) => { 64 | this.loading = false; // Stop loading in case of error 65 | if (error.status === 401) { 66 | this.isAuthenticated = false; // Set flag to false on unauthorized access 67 | this.errorMessage = 'Unauthorized access. Please log in.'; 68 | } else { 69 | this.errorMessage = 70 | 'Error fetching data due to expired token. Please try registering and logging in again.'; 71 | } 72 | this.coursesLength = 10; // Fallback values in case of error 73 | this.lessonsLength = 10; 74 | }, 75 | ); 76 | } 77 | 78 | fetchUserDetails(): void { 79 | const token = localStorage.getItem('authToken'); 80 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 81 | 82 | // Fetch the entire list of users and match by ID 83 | const userDetailsRequest = this.http.get(`${this.apiUrl}users/`, { 84 | headers, 85 | }); 86 | 87 | userDetailsRequest.subscribe( 88 | (userList: any) => { 89 | if (Array.isArray(userList)) { 90 | this.enrollments.forEach((enrollment) => { 91 | const matchedUser = userList.find( 92 | (user: any) => user.id === enrollment.student, 93 | ); 94 | if (matchedUser) { 95 | enrollment.student = matchedUser.username; 96 | } 97 | }); 98 | this.fetchCourseTitles(); // Fetch course titles after fetching user details 99 | } 100 | }, 101 | (error) => { 102 | this.loading = false; // Stop loading even in case of error 103 | console.error('Error fetching user details', error); 104 | }, 105 | ); 106 | } 107 | 108 | fetchCourseTitles(): void { 109 | const token = localStorage.getItem('authToken'); 110 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 111 | 112 | const coursesRequest = this.http.get(`${this.apiUrl}courses/`, { headers }); 113 | 114 | coursesRequest.subscribe( 115 | (courseList: any) => { 116 | if (Array.isArray(courseList)) { 117 | this.enrollments.forEach((enrollment) => { 118 | const matchedCourse = courseList.find( 119 | (course: any) => course.id === enrollment.course, 120 | ); 121 | if (matchedCourse) { 122 | enrollment.course = matchedCourse.title; 123 | } 124 | }); 125 | this.loading = false; 126 | if (this.isAuthenticated) { 127 | // Ensure the chart is only rendered if authenticated 128 | this.renderChart(); 129 | } 130 | } 131 | }, 132 | (error) => { 133 | this.loading = false; // Stop loading even in case of error 134 | console.error('Error fetching course details', error); 135 | }, 136 | ); 137 | } 138 | 139 | renderChart(): void { 140 | const ctx = document.getElementById('enrollmentChart') as HTMLCanvasElement; 141 | 142 | if (this.chart) { 143 | this.chart.destroy(); // Destroy any existing chart instance before creating a new one 144 | } 145 | 146 | const chartConfig: ChartConfiguration<'pie'> = { 147 | type: 'pie', 148 | data: { 149 | labels: ['Lessons', 'Courses', 'Enrollments'], 150 | datasets: [ 151 | { 152 | data: [ 153 | this.lessonsLength, 154 | this.coursesLength, 155 | this.enrollments.length, 156 | ], 157 | backgroundColor: ['#007bff', '#ffc107', '#28a745'], 158 | }, 159 | ], 160 | }, 161 | options: { 162 | responsive: true, 163 | plugins: { 164 | legend: { 165 | position: 'bottom', 166 | }, 167 | }, 168 | }, 169 | }; 170 | 171 | this.chart = new Chart(ctx, chartConfig); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/lesson-list/lesson-list.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1200px; 3 | margin: auto; 4 | } 5 | 6 | .chart-section { 7 | margin-bottom: 20px; 8 | display: flex; 9 | justify-content: center; 10 | margin-top: 20px; 11 | } 12 | 13 | .chart-container { 14 | width: 300px; 15 | height: 300px; 16 | } 17 | 18 | .card { 19 | border: none; 20 | transition: 21 | transform 0.3s, 22 | box-shadow 0.3s; 23 | padding: 15px; 24 | } 25 | 26 | .card:hover { 27 | transform: translateY(-5px); 28 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .loading-overlay { 32 | position: fixed; /* Cover the entire viewport */ 33 | top: 0; 34 | left: 0; 35 | width: 100vw; 36 | height: 100vh; 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | background-color: rgba( 41 | 255, 42 | 255, 43 | 255, 44 | 0.8 45 | ); /* Optional: light overlay effect */ 46 | z-index: 9999; /* Ensure it stays on top */ 47 | } 48 | 49 | .spinner-border { 50 | width: 3rem; 51 | height: 3rem; 52 | } 53 | 54 | .card-title { 55 | font-size: 1.5rem; 56 | font-weight: 700; 57 | margin-bottom: 10px; 58 | } 59 | 60 | .card-text { 61 | color: #666; 62 | font-size: 0.95rem; 63 | } 64 | 65 | .rounded-custom { 66 | border-radius: 15px; 67 | margin: 15px; 68 | } 69 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/lesson-list/lesson-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Platform Overview

3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | Loading... 15 |
16 |
17 | 18 | 19 |
20 |

Lessons

21 |
26 | {{ errorMessage }} 27 |
28 |
29 | No lessons available. 30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |

{{ lesson.title }}

39 |

{{ lesson.content }}

40 |
41 |

Duration: 75 minutes

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/lesson-list/lesson-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { LessonService } from '../../services/lesson.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Chart, ChartConfiguration, registerables } from 'chart.js'; 5 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 6 | import { forkJoin } from 'rxjs'; 7 | 8 | Chart.register(...registerables); 9 | 10 | @Component({ 11 | selector: 'app-lesson-list', 12 | standalone: true, 13 | imports: [CommonModule], 14 | templateUrl: './lesson-list.component.html', 15 | styleUrls: ['./lesson-list.component.css'], 16 | }) 17 | export class LessonListComponent implements OnInit { 18 | lessons: any[] = []; 19 | errorMessage: string = ''; 20 | loading: boolean = true; // Track loading state 21 | chart: Chart<'pie'> | undefined; 22 | private apiUrl = 23 | 'https://learning-management-system-fullstack.onrender.com/api/'; 24 | enrollmentsLength: number = 0; 25 | coursesLength: number = 0; 26 | 27 | constructor( 28 | private lessonService: LessonService, 29 | private http: HttpClient, 30 | ) {} 31 | 32 | ngOnInit(): void { 33 | this.lessonService.getLessons().subscribe( 34 | (data) => { 35 | this.lessons = data; 36 | this.fetchAllData(); 37 | }, 38 | (error) => { 39 | this.loading = false; // Stop loading in case of error 40 | if (error.status === 401) { 41 | this.errorMessage = 'Unauthorized access. Please log in.'; 42 | } else { 43 | this.errorMessage = 'Error fetching lessons.'; 44 | } 45 | }, 46 | ); 47 | } 48 | 49 | fetchAllData(): void { 50 | const token = localStorage.getItem('authToken'); 51 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 52 | 53 | const enrollments$ = this.http.get(`${this.apiUrl}enrollments/`, { 54 | headers, 55 | }); 56 | const courses$ = this.http.get(`${this.apiUrl}courses/`, { headers }); 57 | 58 | forkJoin([enrollments$, courses$]).subscribe( 59 | ([enrollmentsData, coursesData]: [any, any]) => { 60 | this.enrollmentsLength = enrollmentsData.length; 61 | this.coursesLength = coursesData.length; 62 | this.loading = false; // Stop loading once data is fetched 63 | this.renderChart(); // Render the chart after data fetching is complete 64 | }, 65 | (error) => { 66 | this.loading = false; // Stop loading even in case of error 67 | if (error.status === 401) { 68 | this.errorMessage = 'Unauthorized access. Please log in.'; 69 | this.enrollmentsLength = 30; 70 | this.coursesLength = 10; 71 | } else { 72 | this.errorMessage = 73 | 'Error fetching data due to expired token. Please try registering and logging in again.'; 74 | this.enrollmentsLength = 30; 75 | this.coursesLength = 10; 76 | } 77 | this.renderChart(); 78 | }, 79 | ); 80 | } 81 | 82 | renderChart(): void { 83 | const ctx = document.getElementById('lessonChart') as HTMLCanvasElement; 84 | 85 | if (this.chart) { 86 | this.chart.destroy(); 87 | } 88 | 89 | const chartConfig: ChartConfiguration<'pie'> = { 90 | type: 'pie', 91 | data: { 92 | labels: ['Lessons', 'Courses', 'Enrollments'], 93 | datasets: [ 94 | { 95 | data: [ 96 | this.lessons.length, 97 | this.coursesLength, 98 | this.enrollmentsLength, 99 | ], 100 | backgroundColor: ['#007bff', '#ffc107', '#28a745'], 101 | }, 102 | ], 103 | }, 104 | options: { 105 | responsive: true, 106 | plugins: { 107 | legend: { 108 | position: 'bottom', 109 | }, 110 | }, 111 | }, 112 | }; 113 | 114 | this.chart = new Chart(ctx, chartConfig); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/progress-list/progress-list.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1200px; 3 | margin: auto; 4 | } 5 | 6 | .chart-section { 7 | margin-bottom: 20px; 8 | display: flex; 9 | justify-content: center; 10 | margin-top: 20px; 11 | } 12 | 13 | .chart-container { 14 | width: 300px; 15 | height: 300px; 16 | } 17 | 18 | .card { 19 | border: none; 20 | transition: 21 | transform 0.3s, 22 | box-shadow 0.3s; 23 | padding: 15px; 24 | } 25 | 26 | .card:hover { 27 | transform: translateY(-5px); 28 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .card-title { 32 | font-size: 1.5rem; 33 | font-weight: 700; 34 | margin-bottom: 10px; 35 | } 36 | 37 | .card-text { 38 | color: #666; 39 | font-size: 0.95rem; 40 | } 41 | 42 | .rounded-custom { 43 | border-radius: 15px; 44 | margin: 15px; 45 | } 46 | 47 | .loading-overlay { 48 | position: fixed; /* Cover the entire viewport */ 49 | top: 0; 50 | left: 0; 51 | width: 100vw; 52 | height: 100vh; 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | background-color: rgba( 57 | 255, 58 | 255, 59 | 255, 60 | 0.8 61 | ); /* Optional: light overlay effect */ 62 | z-index: 9999; /* Ensure it stays on top */ 63 | } 64 | 65 | .spinner-border { 66 | width: 3rem; 67 | height: 3rem; 68 | } 69 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/progress-list/progress-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Platform Overview

3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | Loading... 15 |
16 |
17 | 18 | 19 |
20 |

Progress Records

21 |
22 | {{ errorMessage }} 23 |
24 |
28 | No progress records available. 29 |
30 | 31 | 32 |
33 |
37 |
38 |
39 |
40 |

41 | User: {{ progress.student }} 42 |

43 |

44 | Lesson: {{ progress.lesson }} 45 |

46 |

47 | Status: 48 | {{ progress.completed ? "Completed" : "Not Completed" }} 49 |

50 |
51 |

52 | Completed At: {{ progress.completed_at }} 53 |

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/progress-list/progress-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ProgressService } from '../../services/progress.service'; 3 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 4 | import { CommonModule } from '@angular/common'; 5 | import { forkJoin } from 'rxjs'; 6 | import { Chart, ChartConfiguration, registerables } from 'chart.js'; 7 | 8 | interface Progress { 9 | _id: string; 10 | student: string; 11 | lesson: string; 12 | completed: boolean; 13 | completed_at: string | null; 14 | } 15 | 16 | interface MappedProgress { 17 | _id: string; 18 | student: string; 19 | lesson: string; 20 | completed: boolean; 21 | completed_at: string; 22 | } 23 | 24 | Chart.register(...registerables); 25 | 26 | @Component({ 27 | selector: 'app-progress-list', 28 | standalone: true, 29 | imports: [CommonModule], 30 | templateUrl: './progress-list.component.html', 31 | styleUrls: ['./progress-list.component.css'], 32 | }) 33 | export class ProgressListComponent implements OnInit { 34 | progressRecords: MappedProgress[] = []; // This will now hold the new list of mapped progress data 35 | errorMessage: string = ''; 36 | loading: boolean = true; // Track loading state 37 | chart: Chart<'pie'> | undefined; 38 | private apiUrl = 39 | 'https://learning-management-system-fullstack.onrender.com/api/'; 40 | lessonsLength: number = 0; 41 | usersLength: number = 0; 42 | 43 | constructor( 44 | private progressService: ProgressService, 45 | private http: HttpClient, 46 | ) {} 47 | 48 | ngOnInit(): void { 49 | this.fetchAllData(); 50 | } 51 | 52 | fetchAllData(): void { 53 | const token = localStorage.getItem('authToken'); 54 | const headers = new HttpHeaders().set('Authorization', `Token ${token}`); 55 | 56 | const progress$ = this.progressService.getProgress(); 57 | const lessons$ = this.http.get(`${this.apiUrl}lessons/`, { 58 | headers, 59 | }); // Fetch lessons 60 | const users$ = this.http.get(`${this.apiUrl}users/`, { headers }); // Fetch users 61 | 62 | forkJoin([progress$, lessons$, users$]).subscribe( 63 | ([progressData, lessonsData, usersData]: [Progress[], any[], any[]]) => { 64 | this.lessonsLength = lessonsData.length; 65 | this.usersLength = usersData.length; 66 | 67 | this.progressRecords = this.mapUserAndLessonDetails( 68 | progressData, 69 | usersData, 70 | lessonsData, 71 | ); // Create a new list 72 | this.loading = false; 73 | this.renderChart(); 74 | }, 75 | (error) => { 76 | this.loading = false; 77 | this.errorMessage = 'Unauthorized access. Please log in.'; 78 | }, 79 | ); 80 | } 81 | 82 | // Helper function to get a random item from an array 83 | getRandomItem(arr: any[]): any { 84 | const randomIndex = Math.floor(Math.random() * arr.length); 85 | return arr[randomIndex]; 86 | } 87 | 88 | // Create a new list of progresses with random student names and lesson titles 89 | mapUserAndLessonDetails( 90 | progressData: Progress[], 91 | usersData: any[], 92 | lessonsData: any[], 93 | ): MappedProgress[] { 94 | return progressData.map((progress) => { 95 | const randomUser = this.getRandomItem(usersData); 96 | const randomLesson = this.getRandomItem(lessonsData); 97 | 98 | return { 99 | _id: progress._id, 100 | student: randomUser ? randomUser.username : 'Unknown User', // Assign random user 101 | lesson: randomLesson ? randomLesson.title : 'Unknown Lesson', // Assign random lesson 102 | completed: progress.completed, 103 | completed_at: progress.completed_at 104 | ? new Date(progress.completed_at).toLocaleDateString() 105 | : 'N/A', // Format date or show 'N/A' 106 | }; 107 | }); 108 | } 109 | 110 | renderChart(): void { 111 | const ctx = document.getElementById('progressChart') as HTMLCanvasElement; 112 | 113 | if (this.chart) { 114 | this.chart.destroy(); // Destroy any existing chart instance before creating a new one 115 | } 116 | 117 | const chartConfig: ChartConfiguration<'pie'> = { 118 | type: 'pie', 119 | data: { 120 | labels: ['Completed', 'Not Completed'], 121 | datasets: [ 122 | { 123 | data: [ 124 | this.progressRecords.filter((record) => record.completed).length, 125 | this.progressRecords.filter((record) => !record.completed).length, 126 | ], 127 | backgroundColor: ['#28a745', '#dc3545'], 128 | }, 129 | ], 130 | }, 131 | options: { 132 | responsive: true, 133 | plugins: { 134 | legend: { 135 | position: 'bottom', 136 | }, 137 | }, 138 | }, 139 | }; 140 | 141 | this.chart = new Chart(ctx, chartConfig); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/user-list/user-list.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1200px; 3 | margin: auto; 4 | } 5 | 6 | .chart-section { 7 | margin-bottom: 20px; 8 | display: flex; 9 | justify-content: center; 10 | margin-top: 20px; 11 | } 12 | 13 | .chart-container { 14 | width: 300px; 15 | height: 300px; 16 | } 17 | 18 | .card { 19 | border: none; 20 | transition: 21 | transform 0.3s, 22 | box-shadow 0.3s; 23 | padding: 15px; 24 | } 25 | 26 | .card:hover { 27 | transform: translateY(-5px); 28 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .card-title { 32 | font-size: 1.5rem; 33 | font-weight: 700; 34 | margin-bottom: 10px; 35 | } 36 | 37 | .card-text { 38 | color: #666; 39 | font-size: 0.95rem; 40 | } 41 | 42 | .rounded-custom { 43 | border-radius: 15px; 44 | margin: 15px; 45 | } 46 | 47 | .loading-overlay { 48 | position: fixed; /* Cover the entire viewport */ 49 | top: 0; 50 | left: 0; 51 | width: 100vw; 52 | height: 100vh; 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | background-color: rgba( 57 | 255, 58 | 255, 59 | 255, 60 | 0.8 61 | ); /* Optional: light overlay effect */ 62 | z-index: 9999; /* Ensure it stays on top */ 63 | } 64 | 65 | .spinner-border { 66 | width: 3rem; 67 | height: 3rem; 68 | } 69 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/user-list/user-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Platform Overview

3 | 4 | 5 |
6 |
7 | Loading... 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |

User List

19 |
20 | {{ errorMessage }} 21 |
22 |
26 | No users available. 27 |
28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |

{{ user.username }}

36 |

{{ user.email }}

37 |

38 | Bio: {{ user.bio }} 39 |

40 |
41 |

42 | Status: 43 | {{ user.is_instructor ? "Instructor" : "Student" }} 44 |

45 |
46 |
47 |
48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/components/user-list/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { UserService } from '../../services/user.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Chart, ChartConfiguration, registerables } from 'chart.js'; 5 | 6 | Chart.register(...registerables); 7 | 8 | @Component({ 9 | selector: 'app-user-list', 10 | standalone: true, 11 | imports: [CommonModule], 12 | templateUrl: './user-list.component.html', 13 | styleUrls: ['./user-list.component.css'], 14 | }) 15 | export class UserListComponent implements OnInit { 16 | users: any[] = []; 17 | errorMessage: string = ''; 18 | loading: boolean = true; // Added to track the loading state 19 | chart: Chart<'pie'> | undefined; 20 | 21 | constructor(private userService: UserService) {} 22 | 23 | ngOnInit(): void { 24 | this.userService.getUsers().subscribe( 25 | (data) => { 26 | this.users = data; 27 | this.loading = false; // Set loading to false once data is fetched 28 | this.renderChart(); 29 | }, 30 | (error) => { 31 | this.loading = false; // Stop loading even if there is an error 32 | if (error.status === 401) { 33 | this.errorMessage = 'Unauthorized access. Please log in.'; 34 | } else { 35 | this.errorMessage = 36 | 'Error fetching data due to expired token. Please try registering and logging in again.'; 37 | } 38 | }, 39 | ); 40 | } 41 | 42 | renderChart(): void { 43 | const ctx = document.getElementById('userChart') as HTMLCanvasElement; 44 | 45 | if (this.chart) { 46 | this.chart.destroy(); 47 | } 48 | 49 | const chartConfig: ChartConfiguration<'pie'> = { 50 | type: 'pie', 51 | data: { 52 | labels: ['Students', 'Instructors'], 53 | datasets: [ 54 | { 55 | data: [ 56 | this.users.filter((user) => !user.is_instructor).length, 57 | this.users.filter((user) => user.is_instructor).length, 58 | ], 59 | backgroundColor: ['#007bff', '#ffc107'], 60 | }, 61 | ], 62 | }, 63 | options: { 64 | responsive: true, 65 | plugins: { 66 | legend: { 67 | position: 'bottom', 68 | }, 69 | }, 70 | }, 71 | }; 72 | 73 | this.chart = new Chart(ctx, chartConfig); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/footer/footer.component.css: -------------------------------------------------------------------------------- 1 | .footer-custom { 2 | background-color: #007bff; 3 | padding: 0; 4 | color: white; 5 | width: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | margin-top: auto; 10 | font-size: 16px; 11 | font-weight: 500; 12 | } 13 | 14 | .footer-links { 15 | list-style-type: none; 16 | padding: 0; 17 | margin: 0; 18 | display: flex; 19 | justify-content: center; 20 | } 21 | 22 | .footer-links li { 23 | margin-right: 15px; 24 | } 25 | 26 | .nav-link { 27 | color: white !important; 28 | text-decoration: none; 29 | margin: 5px 15px; 30 | font-size: 16px; 31 | font-weight: 500; 32 | transition: 33 | color 0.3s, 34 | transform 0.3s; 35 | position: relative; 36 | } 37 | 38 | .nav-link::after { 39 | content: ""; 40 | display: block; 41 | width: 0; 42 | height: 2px; 43 | background: #ffffff; 44 | transition: width 0.3s; 45 | position: absolute; 46 | bottom: -5px; 47 | left: 0; 48 | } 49 | 50 | .nav-link:hover::after { 51 | width: 100%; 52 | } 53 | 54 | .nav-link:hover { 55 | color: #dfe6e9 !important; 56 | transform: scale(1.1); 57 | cursor: pointer; 58 | } 59 | 60 | .active { 61 | font-weight: bold; 62 | color: #ffd700 !important; 63 | } 64 | 65 | /* Styling for social media icons */ 66 | .social-icons { 67 | display: flex; 68 | justify-content: center; 69 | gap: 20px; /* Adds space between the icons */ 70 | } 71 | 72 | .icon-link { 73 | color: #ffffff; 74 | transition: 75 | transform 0.3s ease, 76 | color 0.3s ease; /* Smooth transition on hover */ 77 | } 78 | 79 | .icon-link:hover { 80 | color: #fff; /* Change icon color on hover */ 81 | transform: translateY(-5px); /* Slight bounce effect on hover */ 82 | } 83 | 84 | .icon-link:hover .fa-github { 85 | color: #fff; 86 | } 87 | 88 | .icon-link:hover .fa-linkedin { 89 | color: #fff; 90 | } 91 | 92 | .icon-link:hover .fa-globe { 93 | color: #fff; 94 | } 95 | 96 | .icon-link:hover .fa-envelope { 97 | color: #fff; 98 | } 99 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 94 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Router, RouterModule } from '@angular/router'; 4 | import { Subscription } from 'rxjs'; 5 | import { AuthService } from '../../services/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-footer', 9 | standalone: true, 10 | imports: [CommonModule, RouterModule], 11 | templateUrl: './footer.component.html', 12 | styleUrls: ['./footer.component.css'], 13 | }) 14 | export class FooterComponent implements OnInit, OnDestroy { 15 | private authSubscription: Subscription = new Subscription(); 16 | private loggedInStatus: boolean = false; 17 | currentYear: number = new Date().getFullYear(); 18 | 19 | constructor( 20 | private authService: AuthService, 21 | private router: Router, 22 | ) {} 23 | 24 | ngOnInit(): void { 25 | this.authSubscription = this.authService 26 | .isLoggedIn() 27 | .subscribe((status) => { 28 | this.loggedInStatus = status; 29 | }); 30 | } 31 | 32 | isLoggedIn(): boolean { 33 | return this.loggedInStatus; 34 | } 35 | 36 | logout(): void { 37 | this.authService.logout(); 38 | this.router.navigate(['/login']); 39 | } 40 | 41 | ngOnDestroy(): void { 42 | if (this.authSubscription) { 43 | this.authSubscription.unsubscribe(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/header/header.component.css: -------------------------------------------------------------------------------- 1 | /* Custom Navbar Styling */ 2 | .navbar-custom { 3 | background-color: #007bff; 4 | color: white; 5 | padding: 10px; 6 | width: 100%; 7 | } 8 | 9 | .navbar-brand { 10 | font-size: 24px; 11 | font-weight: bold; 12 | color: white; 13 | text-align: center; 14 | white-space: normal; /* Allow text to wrap */ 15 | max-width: 100%; /* Ensure it wraps within its container */ 16 | margin-right: 0; 17 | } 18 | 19 | .navbar-links { 20 | list-style-type: none; 21 | padding: 0; 22 | margin: 0; 23 | } 24 | 25 | .navbar-links li { 26 | margin-right: 20px; 27 | } 28 | 29 | .nav-link { 30 | color: white !important; 31 | text-decoration: none; 32 | transition: 33 | color 0.3s, 34 | transform 0.3s; 35 | font-size: 16px; 36 | font-weight: 500; 37 | position: relative; 38 | } 39 | 40 | .nav-link::after { 41 | content: ""; 42 | display: block; 43 | width: 0; 44 | height: 2px; 45 | background: #ffffff; 46 | transition: width 0.3s; 47 | position: absolute; 48 | bottom: -5px; 49 | left: 0; 50 | } 51 | 52 | .nav-link:hover::after { 53 | width: 100%; 54 | } 55 | 56 | .nav-link:hover { 57 | color: #dfe6e9 !important; 58 | transform: scale(1.05); 59 | } 60 | 61 | .active { 62 | font-weight: bold; 63 | color: #ffd700 !important; 64 | } 65 | 66 | /*!* Responsive Design Adjustments *!*/ 67 | /*@media (max-width: 991.98px) {*/ 68 | /* .container-fluid {*/ 69 | /* display: flex;*/ 70 | /* flex-direction: column;*/ 71 | /* align-items: center; !* Center align the elements *!*/ 72 | /* }*/ 73 | 74 | /* .navbar-brand {*/ 75 | /* text-align: center;*/ 76 | /* font-size: 24px;*/ 77 | /* white-space: normal; !* Allow text to wrap *!*/ 78 | /* }*/ 79 | 80 | /* .navbar-links {*/ 81 | /* flex-direction: column;*/ 82 | /* align-items: center;*/ 83 | /* }*/ 84 | 85 | /* .navbar-links li {*/ 86 | /* margin-right: 0;*/ 87 | /* margin-bottom: 10px;*/ 88 | /* }*/ 89 | 90 | /* .navbar-toggler {*/ 91 | /* margin-top: 10px;*/ 92 | /* }*/ 93 | /*}*/ 94 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 106 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/core/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { AuthService } from '../../services/auth.service'; 3 | import { Router, RouterModule } from '@angular/router'; 4 | import { CommonModule } from '@angular/common'; 5 | import { Subscription, interval } from 'rxjs'; 6 | import { switchMap, startWith } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-header', 10 | standalone: true, 11 | imports: [RouterModule, CommonModule], 12 | templateUrl: './header.component.html', 13 | styleUrls: ['./header.component.css'], 14 | }) 15 | export class HeaderComponent implements OnInit, OnDestroy { 16 | private authSubscription: Subscription = new Subscription(); 17 | private loggedInStatus: boolean = false; 18 | 19 | constructor( 20 | private authService: AuthService, 21 | private router: Router, 22 | ) {} 23 | 24 | ngOnInit(): void { 25 | // Start with an immediate check, then every 2 seconds 26 | this.authSubscription = interval(2000) 27 | .pipe( 28 | startWith(0), // Immediately triggers the first emission 29 | switchMap(() => this.authService.isTokenValid()), // Check token validity 30 | ) 31 | .subscribe((isValid) => { 32 | this.loggedInStatus = isValid; 33 | }); 34 | } 35 | 36 | isLoggedIn(): boolean { 37 | return this.loggedInStatus; 38 | } 39 | 40 | logout(): void { 41 | this.authService.logout(); 42 | this.loggedInStatus = false; // Update immediately on logout 43 | } 44 | 45 | ngOnDestroy(): void { 46 | if (this.authSubscription) { 47 | this.authSubscription.unsubscribe(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/home/home.component.css: -------------------------------------------------------------------------------- 1 | .home { 2 | max-width: 900px; 3 | margin: auto; 4 | padding: 20px; 5 | } 6 | 7 | .welcome-section { 8 | margin-bottom: 30px; 9 | } 10 | 11 | .welcome-section h1 { 12 | font-size: 2.5rem; 13 | font-weight: 700; 14 | } 15 | 16 | .welcome-section p { 17 | font-size: 1.25rem; 18 | color: #555; 19 | } 20 | 21 | .card { 22 | border: none; 23 | transition: 24 | transform 0.3s, 25 | box-shadow 0.3s; 26 | margin: 10px; 27 | padding: 10px; 28 | } 29 | 30 | .card:hover { 31 | transform: translateY(-5px); 32 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); 33 | } 34 | 35 | .lead { 36 | text-align: center; 37 | } 38 | 39 | .card-title { 40 | font-size: 1.2rem; 41 | font-weight: 600; 42 | margin-top: 10px; 43 | } 44 | 45 | .card-text { 46 | color: #777; 47 | text-align: center; 48 | } 49 | 50 | .btn-outline-primary { 51 | font-weight: 500; 52 | transition: 53 | color 0.3s, 54 | border-color 0.3s, 55 | background-color 0.3s; 56 | padding: 5px; 57 | border-radius: 8px; 58 | } 59 | 60 | .btn-outline-primary:hover { 61 | background-color: #007bff; 62 | color: white; 63 | border-color: #007bff; 64 | } 65 | 66 | .rounded-custom { 67 | border-radius: 15px; 68 | } 69 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

5 | Welcome to the E-Learning Management System 6 |

7 |

8 | Explore courses, track your progress, and manage enrollments easily. 9 |

10 |
11 | 12 | 13 | 142 |
143 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | standalone: true, 7 | imports: [RouterModule], 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.css'], 10 | }) 11 | export class HomeComponent {} 12 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/notfound/notfound.component.css: -------------------------------------------------------------------------------- 1 | .not-found { 2 | margin-top: 5rem; 3 | } 4 | 5 | .not-found h1 { 6 | font-size: 6rem; 7 | font-weight: bold; 8 | } 9 | 10 | .not-found h2 { 11 | font-size: 2.5rem; 12 | font-weight: 600; 13 | } 14 | 15 | .not-found p { 16 | font-size: 1.25rem; 17 | } 18 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/notfound/notfound.component.html: -------------------------------------------------------------------------------- 1 |
2 |

404

3 |

Page Not Found

4 |

Sorry, the page you are looking for does not exist.

5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/pages/notfound/notfound.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-not-found', 6 | templateUrl: './notfound.component.html', 7 | standalone: true, 8 | styleUrls: ['./notfound.component.css'], 9 | }) 10 | export class NotFoundComponent { 11 | constructor(private router: Router) {} 12 | 13 | // Method to navigate back to the home page 14 | goToHome(): void { 15 | this.router.navigate(['/']); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpInterceptor, 4 | HttpRequest, 5 | HttpHandler, 6 | HttpEvent, 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { AuthService } from './auth.service'; 10 | 11 | @Injectable() 12 | export class AuthInterceptor implements HttpInterceptor { 13 | constructor(private authService: AuthService) {} 14 | 15 | intercept( 16 | req: HttpRequest, 17 | next: HttpHandler, 18 | ): Observable> { 19 | const token = this.authService.getToken(); 20 | 21 | if (token) { 22 | const cloned = req.clone({ 23 | setHeaders: { 24 | Authorization: `Token ${token}`, 25 | }, 26 | }); 27 | console.log('AuthInterceptor: Attaching token to request:', cloned); 28 | return next.handle(cloned); 29 | } else { 30 | return next.handle(req); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable, BehaviorSubject, of } from 'rxjs'; 4 | import { catchError, map, tap } from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthService { 10 | private loginUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/auth/login/'; 12 | private registerUrl = 13 | 'https://learning-management-system-fullstack.onrender.com/api/auth/registration/'; // Registration endpoint 14 | private validateUrl = 15 | 'https://learning-management-system-fullstack.onrender.com/api/enrollments/'; // Token validation endpoint 16 | private tokenKey = 'authToken'; 17 | private isLoggedInSubject = new BehaviorSubject(this.hasToken()); 18 | 19 | constructor(private http: HttpClient) {} 20 | 21 | // Login method 22 | login(username: string, password: string): Observable { 23 | return this.http.post(this.loginUrl, { username, password }).pipe( 24 | tap((response: any) => { 25 | if (response && response.key) { 26 | localStorage.setItem(this.tokenKey, response.key); 27 | this.isLoggedInSubject.next(true); 28 | } 29 | }), 30 | ); 31 | } 32 | 33 | // Registration method 34 | register( 35 | username: string, 36 | email: string, 37 | password1: string, 38 | password2: string, 39 | ): Observable { 40 | return this.http 41 | .post(this.registerUrl, { 42 | username, 43 | email, 44 | password1, 45 | password2, 46 | }) 47 | .pipe( 48 | tap((response: any) => { 49 | console.log('Registration successful:', response); 50 | }), 51 | ); 52 | } 53 | 54 | // Logout method 55 | logout(): void { 56 | localStorage.removeItem(this.tokenKey); 57 | this.isLoggedInSubject.next(false); 58 | } 59 | 60 | // Get the stored token 61 | getToken(): string | null { 62 | return localStorage.getItem(this.tokenKey); 63 | } 64 | 65 | // Check if the user is logged in 66 | isLoggedIn(): Observable { 67 | return this.isLoggedInSubject.asObservable(); 68 | } 69 | 70 | // Check if the token is valid by making a test API request 71 | isTokenValid(): Observable { 72 | const token = this.getToken(); 73 | if (!token) { 74 | this.isLoggedInSubject.next(false); 75 | return of(false); // No token means invalid 76 | } 77 | 78 | // Set up headers with the token 79 | const headers = new HttpHeaders({ 80 | Authorization: `Token ${token}`, 81 | 'Content-Type': 'application/json', 82 | }); 83 | 84 | // Make a request to validate the token 85 | return this.http.get(this.validateUrl, { headers }).pipe( 86 | map(() => { 87 | this.isLoggedInSubject.next(true); // Token is valid 88 | return true; 89 | }), 90 | catchError((error) => { 91 | if (error.status === 401) { 92 | this.logout(); // Clear the token if it's invalid 93 | return of(false); 94 | } 95 | return of(true); // Other errors treated as valid for now 96 | }), 97 | ); 98 | } 99 | 100 | // Private method to check if a token is stored 101 | private hasToken(): boolean { 102 | return !!this.getToken(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/course.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class CourseService { 10 | private apiUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/courses/'; 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private authService: AuthService, 16 | ) {} 17 | 18 | // Helper method to get the authorization headers 19 | private getAuthHeaders(): HttpHeaders { 20 | const token = this.authService.getToken(); 21 | return new HttpHeaders({ 22 | Authorization: `Token ${token}`, 23 | 'Content-Type': 'application/json', 24 | }); 25 | } 26 | 27 | // Get all courses 28 | getCourses(): Observable { 29 | const headers = this.getAuthHeaders(); 30 | return this.http.get(this.apiUrl, { headers }); 31 | } 32 | 33 | // Get a specific course by ID 34 | getCourse(id: number): Observable { 35 | const headers = this.getAuthHeaders(); 36 | return this.http.get(`${this.apiUrl}${id}/`, { headers }); 37 | } 38 | 39 | // Create a new course 40 | createCourse(course: any): Observable { 41 | const headers = this.getAuthHeaders(); 42 | return this.http.post(this.apiUrl, course, { headers }); 43 | } 44 | 45 | // Update an existing course by ID 46 | updateCourse(id: number, course: any): Observable { 47 | const headers = this.getAuthHeaders(); 48 | return this.http.put(`${this.apiUrl}${id}/`, course, { headers }); 49 | } 50 | 51 | // Delete a course by ID 52 | deleteCourse(id: number): Observable { 53 | const headers = this.getAuthHeaders(); 54 | return this.http.delete(`${this.apiUrl}${id}/`, { headers }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/enrollment.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class EnrollmentService { 10 | private apiUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/enrollments/'; 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private authService: AuthService, 16 | ) {} 17 | 18 | // Helper method to get the authorization headers 19 | private getAuthHeaders(): HttpHeaders { 20 | const token = this.authService.getToken(); 21 | return new HttpHeaders({ 22 | Authorization: `Token ${token}`, 23 | 'Content-Type': 'application/json', 24 | }); 25 | } 26 | 27 | // Get all enrollments 28 | getEnrollments(): Observable { 29 | const headers = this.getAuthHeaders(); 30 | return this.http.get(this.apiUrl, { headers }); 31 | } 32 | 33 | // Get a specific enrollment by ID 34 | getEnrollment(id: number): Observable { 35 | const headers = this.getAuthHeaders(); 36 | return this.http.get(`${this.apiUrl}${id}/`, { headers }); 37 | } 38 | 39 | // Create a new enrollment 40 | createEnrollment(enrollment: any): Observable { 41 | const headers = this.getAuthHeaders(); 42 | return this.http.post(this.apiUrl, enrollment, { headers }); 43 | } 44 | 45 | // Update an existing enrollment by ID 46 | updateEnrollment(id: number, enrollment: any): Observable { 47 | const headers = this.getAuthHeaders(); 48 | return this.http.put(`${this.apiUrl}${id}/`, enrollment, { headers }); 49 | } 50 | 51 | // Delete an enrollment by ID 52 | deleteEnrollment(id: number): Observable { 53 | const headers = this.getAuthHeaders(); 54 | return this.http.delete(`${this.apiUrl}${id}/`, { headers }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/lesson.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class LessonService { 10 | private apiUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/lessons/'; 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private authService: AuthService, 16 | ) {} 17 | 18 | // Helper method to get the authorization headers 19 | private getAuthHeaders(): HttpHeaders { 20 | const token = this.authService.getToken(); 21 | return new HttpHeaders({ 22 | Authorization: `Token ${token}`, 23 | 'Content-Type': 'application/json', 24 | }); 25 | } 26 | 27 | // Get all lessons 28 | getLessons(): Observable { 29 | const headers = this.getAuthHeaders(); 30 | return this.http.get(this.apiUrl, { headers }); 31 | } 32 | 33 | // Get a specific lesson by ID 34 | getLesson(id: number): Observable { 35 | const headers = this.getAuthHeaders(); 36 | return this.http.get(`${this.apiUrl}${id}/`, { headers }); 37 | } 38 | 39 | // Create a new lesson 40 | createLesson(lesson: any): Observable { 41 | const headers = this.getAuthHeaders(); 42 | return this.http.post(this.apiUrl, lesson, { headers }); 43 | } 44 | 45 | // Update an existing lesson by ID 46 | updateLesson(id: number, lesson: any): Observable { 47 | const headers = this.getAuthHeaders(); 48 | return this.http.put(`${this.apiUrl}${id}/`, lesson, { headers }); 49 | } 50 | 51 | // Delete a lesson by ID 52 | deleteLesson(id: number): Observable { 53 | const headers = this.getAuthHeaders(); 54 | return this.http.delete(`${this.apiUrl}${id}/`, { headers }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/progress.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ProgressService { 10 | private apiUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/progress/'; 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private authService: AuthService, 16 | ) {} 17 | 18 | // Helper method to get the authorization headers 19 | private getAuthHeaders(): HttpHeaders { 20 | const token = this.authService.getToken(); 21 | return new HttpHeaders({ 22 | Authorization: `Token ${token}`, 23 | 'Content-Type': 'application/json', 24 | }); 25 | } 26 | 27 | // Get all progress records 28 | getProgress(): Observable { 29 | const headers = this.getAuthHeaders(); 30 | return this.http.get(this.apiUrl, { headers }); 31 | } 32 | 33 | // Get a specific progress record by ID 34 | getProgressRecord(id: number): Observable { 35 | const headers = this.getAuthHeaders(); 36 | return this.http.get(`${this.apiUrl}${id}/`, { headers }); 37 | } 38 | 39 | // Create a new progress record 40 | createProgress(progress: any): Observable { 41 | const headers = this.getAuthHeaders(); 42 | return this.http.post(this.apiUrl, progress, { headers }); 43 | } 44 | 45 | // Update an existing progress record by ID 46 | updateProgress(id: number, progress: any): Observable { 47 | const headers = this.getAuthHeaders(); 48 | return this.http.put(`${this.apiUrl}${id}/`, progress, { headers }); 49 | } 50 | 51 | // Delete a progress record by ID 52 | deleteProgress(id: number): Observable { 53 | const headers = this.getAuthHeaders(); 54 | return this.http.delete(`${this.apiUrl}${id}/`, { headers }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class UserService { 10 | private apiUrl = 11 | 'https://learning-management-system-fullstack.onrender.com/api/users/'; 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private authService: AuthService, 16 | ) {} 17 | 18 | private getAuthHeaders(): HttpHeaders { 19 | const token = this.authService.getToken(); 20 | return new HttpHeaders({ 21 | Authorization: `Token ${token}`, 22 | 'Content-Type': 'application/json', 23 | }); 24 | } 25 | 26 | getUsers(): Observable { 27 | const headers = this.getAuthHeaders(); 28 | return this.http.get(this.apiUrl, { headers }); 29 | } 30 | 31 | getUser(id: number): Observable { 32 | const headers = this.getAuthHeaders(); 33 | return this.http.get(`${this.apiUrl}${id}/`, { headers }); 34 | } 35 | 36 | createUser(user: any): Observable { 37 | const headers = this.getAuthHeaders(); 38 | return this.http.post(this.apiUrl, user, { headers }); 39 | } 40 | 41 | updateUser(id: number, user: any): Observable { 42 | const headers = this.getAuthHeaders(); 43 | return this.http.put(`${this.apiUrl}${id}/`, user, { headers }); 44 | } 45 | 46 | deleteUser(id: number): Observable { 47 | const headers = this.getAuthHeaders(); 48 | return this.http.delete(`${this.apiUrl}${id}/`, { headers }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/apple-touch-icon.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/assets/.gitkeep -------------------------------------------------------------------------------- /LMS-Frontend/app/src/assets/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/assets/icon-192x192.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/assets/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/assets/icon-512x512.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/favicon-16x16.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/favicon-32x32.png -------------------------------------------------------------------------------- /LMS-Frontend/app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/LMS-Frontend/app/src/favicon.ico -------------------------------------------------------------------------------- /LMS-Frontend/app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Learning Management System - A Platform to Learn and Teach 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import { 4 | provideHttpClient, 5 | HTTP_INTERCEPTORS, 6 | withFetch, 7 | } from '@angular/common/http'; 8 | import { provideRouter } from '@angular/router'; 9 | import { routes } from './app/app.routes'; 10 | import { AuthInterceptor } from './app/services/auth.interceptor'; 11 | 12 | bootstrapApplication(AppComponent, { 13 | providers: [ 14 | provideRouter(routes), 15 | provideHttpClient(withFetch()), 16 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 17 | ], 18 | }).catch((err) => console.error(err)); 19 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "E-Learning Management System", 3 | "short_name": "LMS", 4 | "description": "A Progressive Web App for E-Learning Management System", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "scope": "/", 8 | "background_color": "#ffffff", 9 | "theme_color": "#007bff", 10 | "icons": [ 11 | { 12 | "src": "assets/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "assets/icon-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LMS-Frontend/app/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", sans-serif; 3 | background-color: #f4f4f9; 4 | color: #333; 5 | margin: 0; 6 | padding: 0; 7 | overflow-x: hidden; 8 | } 9 | 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6 { 16 | font-weight: 600; 17 | } 18 | 19 | p, 20 | li, 21 | a { 22 | font-weight: 400; 23 | line-height: 1.6; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6 { 32 | margin: 0; 33 | padding-bottom: 10px; 34 | border-bottom: 2px solid #ddd; 35 | color: #444; 36 | } 37 | 38 | .container { 39 | padding: 20px; 40 | margin-top: 20px; 41 | } 42 | 43 | button { 44 | background-color: #007bff; 45 | border: none; 46 | color: white; 47 | padding: 10px 20px; 48 | text-align: center; 49 | text-decoration: none; 50 | display: inline-block; 51 | font-size: 16px; 52 | margin: 4px 2px; 53 | cursor: pointer; 54 | border-radius: 5px; 55 | } 56 | -------------------------------------------------------------------------------- /LMS-Frontend/app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LMS-Frontend/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LMS-Frontend/app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | build: 4 | context: ./LMS-Backend 5 | dockerfile: Dockerfile 6 | command: python manage.py runserver 0.0.0.0:8000 7 | volumes: 8 | - ./LMS-Backend:/app 9 | ports: 10 | - "8000:8000" 11 | depends_on: 12 | - mongodb 13 | - redis 14 | environment: 15 | - MONGO_HOST=mongodb 16 | - MONGO_PORT=27017 17 | - REDIS_HOST=redis 18 | - REDIS_PORT=6379 19 | 20 | frontend: 21 | build: 22 | context: ./LMS-Frontend 23 | dockerfile: Dockerfile 24 | ports: 25 | - "8080:8080" 26 | volumes: 27 | - ./LMS-Frontend:/app 28 | stdin_open: true 29 | tty: true 30 | 31 | mongodb: 32 | image: mongo:5.0 33 | container_name: mongo 34 | ports: 35 | - "27017:27017" 36 | volumes: 37 | - mongodb_data:/data/db 38 | 39 | redis: 40 | image: redis:6 41 | container_name: redis 42 | ports: 43 | - "6379:6379" 44 | 45 | volumes: 46 | mongodb_data: 47 | -------------------------------------------------------------------------------- /docs/admin-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/admin-ui.png -------------------------------------------------------------------------------- /docs/browsable-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/browsable-api.png -------------------------------------------------------------------------------- /docs/course-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/course-ui.png -------------------------------------------------------------------------------- /docs/enrollments-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/enrollments-ui.png -------------------------------------------------------------------------------- /docs/footer-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/footer-ui.png -------------------------------------------------------------------------------- /docs/gui-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/gui-tools.png -------------------------------------------------------------------------------- /docs/home-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/home-ui.png -------------------------------------------------------------------------------- /docs/lesson-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/lesson-ui.png -------------------------------------------------------------------------------- /docs/login-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/login-ui.png -------------------------------------------------------------------------------- /docs/mobile-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/mobile-ui.png -------------------------------------------------------------------------------- /docs/notfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/notfound.png -------------------------------------------------------------------------------- /docs/progress-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/progress-ui.png -------------------------------------------------------------------------------- /docs/redoc-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/redoc-ui.png -------------------------------------------------------------------------------- /docs/register-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/register-ui.png -------------------------------------------------------------------------------- /docs/swagger-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/swagger-ui.png -------------------------------------------------------------------------------- /docs/unauthorized-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/unauthorized-ui.png -------------------------------------------------------------------------------- /docs/user-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/docs/user-ui.png -------------------------------------------------------------------------------- /identifier.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangsonww/Learning-Management-System-Fullstack/fb61e29f64c690e32e50614b1253165352ae6526/identifier.sqlite -------------------------------------------------------------------------------- /kubernetes/backend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend-deployment 5 | labels: 6 | app: backend 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: backend 12 | template: 13 | metadata: 14 | labels: 15 | app: backend 16 | spec: 17 | containers: 18 | - name: backend 19 | image: lms-backend:latest # Build and push this to a container registry 20 | ports: 21 | - containerPort: 3000 22 | env: 23 | - name: NODE_ENV 24 | value: "production" 25 | volumeMounts: 26 | - name: backend-code 27 | mountPath: /app 28 | volumes: 29 | - name: backend-code 30 | hostPath: 31 | path: /home/user/project/backend 32 | -------------------------------------------------------------------------------- /kubernetes/backend-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backend-service 5 | spec: 6 | selector: 7 | app: backend 8 | ports: 9 | - protocol: TCP 10 | port: 3000 11 | targetPort: 3000 12 | type: ClusterIP # Internal service 13 | -------------------------------------------------------------------------------- /kubernetes/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: lms-app-config 5 | data: 6 | NODE_ENV: "production" 7 | REACT_APP_BACKEND_URL: "http://backend-service:3000" 8 | -------------------------------------------------------------------------------- /kubernetes/frontend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend-deployment 5 | labels: 6 | app: frontend 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: frontend 12 | template: 13 | metadata: 14 | labels: 15 | app: frontend 16 | spec: 17 | containers: 18 | - name: frontend 19 | image: lms-frontend:latest # Build and push this to a container registry 20 | ports: 21 | - containerPort: 3001 22 | env: 23 | - name: ANGULAR_APP_BACKEND_URL 24 | value: "http://backend-service:3000" # This service will route to the backend service 25 | volumeMounts: 26 | - name: frontend-code 27 | mountPath: /app 28 | volumes: 29 | - name: frontend-code 30 | hostPath: 31 | path: /home/user/project/frontend 32 | -------------------------------------------------------------------------------- /kubernetes/frontend-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend-service 5 | spec: 6 | selector: 7 | app: frontend 8 | ports: 9 | - protocol: TCP 10 | port: 3001 11 | targetPort: 3001 12 | type: NodePort # Expose frontend for external access 13 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image for nginx 2 | FROM nginx:latest 3 | 4 | # Remove the default nginx config file 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | 7 | # Copy the custom nginx configuration file 8 | COPY nginx.conf /etc/nginx/conf.d/ 9 | 10 | # Expose port 80 11 | EXPOSE 80 12 | 13 | # Start Nginx 14 | CMD ["nginx", "-g", "daemon off;"] 15 | -------------------------------------------------------------------------------- /nginx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | nginx: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "80:80" # Map host port 80 to container port 80 10 | volumes: 11 | - ./nginx.conf:/etc/nginx/conf.d/nginx.conf # Mount custom nginx config file 12 | restart: always # Automatically restart container if it crashes 13 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Load Balancer Configuration 2 | http { 3 | upstream django_backend { 4 | server moodify-emotion-music-app.onrender.com; # Default port: 10000 5 | server moodify-emotion-music-app.onrender.com:8000; 6 | server moodify-emotion-music-app.onrender.com:8001; 7 | server moodify-emotion-music-app.onrender.com:8002; 8 | } 9 | 10 | # Server block for load balancing 11 | server { 12 | listen 80; 13 | 14 | # Define the root for the requests, forward to upstream 15 | location / { 16 | proxy_pass http://django_backend; 17 | 18 | # Set proxy headers 19 | proxy_set_header Host $host; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_set_header X-Forwarded-Proto $scheme; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nginx/start_nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the docker-compose file location 4 | COMPOSE_FILE="docker-compose.yml" 5 | 6 | # Display help message 7 | function show_help() { 8 | echo "Usage: $0 [option]" 9 | echo "Options:" 10 | echo " build Build the Nginx Docker image" 11 | echo " start Start the Nginx container" 12 | echo " stop Stop the Nginx container" 13 | echo " restart Restart the Nginx container" 14 | echo " logs Show logs of the Nginx container" 15 | echo " clean Stop and remove the container" 16 | echo " help Display this help message" 17 | } 18 | 19 | # Check if docker-compose file exists 20 | if [ ! -f "$COMPOSE_FILE" ]; then 21 | echo "Error: $COMPOSE_FILE not found!" 22 | exit 1 23 | fi 24 | 25 | # Handle script arguments 26 | case "$1" in 27 | build) 28 | echo "Building the Nginx Docker image..." 29 | docker-compose -f $COMPOSE_FILE build 30 | ;; 31 | start) 32 | echo "Starting the Nginx container..." 33 | docker-compose -f $COMPOSE_FILE up -d 34 | ;; 35 | stop) 36 | echo "Stopping the Nginx container..." 37 | docker-compose -f $COMPOSE_FILE down 38 | ;; 39 | restart) 40 | echo "Restarting the Nginx container..." 41 | docker-compose -f $COMPOSE_FILE down 42 | docker-compose -f $COMPOSE_FILE up -d 43 | ;; 44 | logs) 45 | echo "Displaying logs of the Nginx container..." 46 | docker-compose -f $COMPOSE_FILE logs -f 47 | ;; 48 | clean) 49 | echo "Cleaning up: stopping and removing the container..." 50 | docker-compose -f $COMPOSE_FILE down -v 51 | ;; 52 | help) 53 | show_help 54 | ;; 55 | *) 56 | echo "Invalid option!" 57 | show_help 58 | ;; 59 | esac 60 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-management-system", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "learning-management-system", 9 | "version": "1.0.0", 10 | "hasInstallScript": true, 11 | "license": "MIT", 12 | "dependencies": { 13 | "concurrently": "^9.0.1", 14 | "prettier": "^3.3.3" 15 | } 16 | }, 17 | "node_modules/ansi-regex": { 18 | "version": "5.0.1", 19 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 20 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 21 | "engines": { 22 | "node": ">=8" 23 | } 24 | }, 25 | "node_modules/ansi-styles": { 26 | "version": "4.3.0", 27 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 28 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 29 | "dependencies": { 30 | "color-convert": "^2.0.1" 31 | }, 32 | "engines": { 33 | "node": ">=8" 34 | }, 35 | "funding": { 36 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 37 | } 38 | }, 39 | "node_modules/chalk": { 40 | "version": "4.1.2", 41 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 42 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 43 | "dependencies": { 44 | "ansi-styles": "^4.1.0", 45 | "supports-color": "^7.1.0" 46 | }, 47 | "engines": { 48 | "node": ">=10" 49 | }, 50 | "funding": { 51 | "url": "https://github.com/chalk/chalk?sponsor=1" 52 | } 53 | }, 54 | "node_modules/chalk/node_modules/supports-color": { 55 | "version": "7.2.0", 56 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 57 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 58 | "dependencies": { 59 | "has-flag": "^4.0.0" 60 | }, 61 | "engines": { 62 | "node": ">=8" 63 | } 64 | }, 65 | "node_modules/cliui": { 66 | "version": "8.0.1", 67 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 68 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 69 | "dependencies": { 70 | "string-width": "^4.2.0", 71 | "strip-ansi": "^6.0.1", 72 | "wrap-ansi": "^7.0.0" 73 | }, 74 | "engines": { 75 | "node": ">=12" 76 | } 77 | }, 78 | "node_modules/color-convert": { 79 | "version": "2.0.1", 80 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 81 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 82 | "dependencies": { 83 | "color-name": "~1.1.4" 84 | }, 85 | "engines": { 86 | "node": ">=7.0.0" 87 | } 88 | }, 89 | "node_modules/color-name": { 90 | "version": "1.1.4", 91 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 92 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 93 | }, 94 | "node_modules/concurrently": { 95 | "version": "9.0.1", 96 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz", 97 | "integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==", 98 | "dependencies": { 99 | "chalk": "^4.1.2", 100 | "lodash": "^4.17.21", 101 | "rxjs": "^7.8.1", 102 | "shell-quote": "^1.8.1", 103 | "supports-color": "^8.1.1", 104 | "tree-kill": "^1.2.2", 105 | "yargs": "^17.7.2" 106 | }, 107 | "bin": { 108 | "conc": "dist/bin/concurrently.js", 109 | "concurrently": "dist/bin/concurrently.js" 110 | }, 111 | "engines": { 112 | "node": ">=18" 113 | }, 114 | "funding": { 115 | "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" 116 | } 117 | }, 118 | "node_modules/emoji-regex": { 119 | "version": "8.0.0", 120 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 121 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 122 | }, 123 | "node_modules/escalade": { 124 | "version": "3.2.0", 125 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 126 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 127 | "engines": { 128 | "node": ">=6" 129 | } 130 | }, 131 | "node_modules/get-caller-file": { 132 | "version": "2.0.5", 133 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 134 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 135 | "engines": { 136 | "node": "6.* || 8.* || >= 10.*" 137 | } 138 | }, 139 | "node_modules/has-flag": { 140 | "version": "4.0.0", 141 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 142 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 143 | "engines": { 144 | "node": ">=8" 145 | } 146 | }, 147 | "node_modules/is-fullwidth-code-point": { 148 | "version": "3.0.0", 149 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 150 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 151 | "engines": { 152 | "node": ">=8" 153 | } 154 | }, 155 | "node_modules/lodash": { 156 | "version": "4.17.21", 157 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 158 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 159 | }, 160 | "node_modules/prettier": { 161 | "version": "3.3.3", 162 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", 163 | "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", 164 | "bin": { 165 | "prettier": "bin/prettier.cjs" 166 | }, 167 | "engines": { 168 | "node": ">=14" 169 | }, 170 | "funding": { 171 | "url": "https://github.com/prettier/prettier?sponsor=1" 172 | } 173 | }, 174 | "node_modules/require-directory": { 175 | "version": "2.1.1", 176 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 177 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 178 | "engines": { 179 | "node": ">=0.10.0" 180 | } 181 | }, 182 | "node_modules/rxjs": { 183 | "version": "7.8.1", 184 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", 185 | "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", 186 | "dependencies": { 187 | "tslib": "^2.1.0" 188 | } 189 | }, 190 | "node_modules/shell-quote": { 191 | "version": "1.8.1", 192 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", 193 | "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", 194 | "funding": { 195 | "url": "https://github.com/sponsors/ljharb" 196 | } 197 | }, 198 | "node_modules/string-width": { 199 | "version": "4.2.3", 200 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 201 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 202 | "dependencies": { 203 | "emoji-regex": "^8.0.0", 204 | "is-fullwidth-code-point": "^3.0.0", 205 | "strip-ansi": "^6.0.1" 206 | }, 207 | "engines": { 208 | "node": ">=8" 209 | } 210 | }, 211 | "node_modules/strip-ansi": { 212 | "version": "6.0.1", 213 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 214 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 215 | "dependencies": { 216 | "ansi-regex": "^5.0.1" 217 | }, 218 | "engines": { 219 | "node": ">=8" 220 | } 221 | }, 222 | "node_modules/supports-color": { 223 | "version": "8.1.1", 224 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 225 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 226 | "dependencies": { 227 | "has-flag": "^4.0.0" 228 | }, 229 | "engines": { 230 | "node": ">=10" 231 | }, 232 | "funding": { 233 | "url": "https://github.com/chalk/supports-color?sponsor=1" 234 | } 235 | }, 236 | "node_modules/tree-kill": { 237 | "version": "1.2.2", 238 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 239 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 240 | "bin": { 241 | "tree-kill": "cli.js" 242 | } 243 | }, 244 | "node_modules/tslib": { 245 | "version": "2.8.0", 246 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", 247 | "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" 248 | }, 249 | "node_modules/wrap-ansi": { 250 | "version": "7.0.0", 251 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 252 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 253 | "dependencies": { 254 | "ansi-styles": "^4.0.0", 255 | "string-width": "^4.1.0", 256 | "strip-ansi": "^6.0.0" 257 | }, 258 | "engines": { 259 | "node": ">=10" 260 | }, 261 | "funding": { 262 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 263 | } 264 | }, 265 | "node_modules/y18n": { 266 | "version": "5.0.8", 267 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 268 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 269 | "engines": { 270 | "node": ">=10" 271 | } 272 | }, 273 | "node_modules/yargs": { 274 | "version": "17.7.2", 275 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 276 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 277 | "dependencies": { 278 | "cliui": "^8.0.1", 279 | "escalade": "^3.1.1", 280 | "get-caller-file": "^2.0.5", 281 | "require-directory": "^2.1.1", 282 | "string-width": "^4.2.3", 283 | "y18n": "^5.0.5", 284 | "yargs-parser": "^21.1.1" 285 | }, 286 | "engines": { 287 | "node": ">=12" 288 | } 289 | }, 290 | "node_modules/yargs-parser": { 291 | "version": "21.1.1", 292 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 293 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 294 | "engines": { 295 | "node": ">=12" 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-management-system", 3 | "version": "1.0.0", 4 | "description": "Learning Management System - a web application for managing courses and students", 5 | "directories": { 6 | "doc": "docs" 7 | }, 8 | "scripts": { 9 | "install": "cd LMS-Frontend && npm install && cd ../LMS-Backend && pip install -r requirements.txt", 10 | "start": "cd LMS-Backend && python manage.py runserver && cd ../LMS-Frontend && npm start", 11 | "build": "cd LMS-Frontend/app && ng build", 12 | "test": "cd LMS-Frontend/app && ng serve", 13 | "eject": "cd LMS-Frontend/app && npm run eject", 14 | "frontend": "cd LMS-Frontend/app && ng serve", 15 | "backend": "cd LMS-Backend && python manage.py runserver", 16 | "dev": "concurrently \"npm run backend\" \"npm run frontend\"", 17 | "format": "prettier --write \"**/*.{ts,css}\"" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/hoangsonww/Learning-Management-System-Fullstack.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/hoangsonww/Learning-Management-System-Fullstack/issues", 25 | "email": "hoangson091104@gmail.com", 26 | "name": "Son Nguyen" 27 | }, 28 | "keywords": [ 29 | "LMS", 30 | "Learning Management System", 31 | "React", 32 | "Django" 33 | ], 34 | "author": "Son Nguyen", 35 | "license": "MIT", 36 | "dependencies": { 37 | "concurrently": "^9.0.1", 38 | "prettier": "^3.3.3" 39 | } 40 | } 41 | --------------------------------------------------------------------------------