diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..554142d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*venv +*.idea +*.pyc +*__pycache__ +**/node_modules/ \ No newline at end of file diff --git a/GLITCH-assignment.pdf b/GLITCH-assignment.pdf new file mode 100644 index 0000000..f386499 Binary files /dev/null and b/GLITCH-assignment.pdf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8470255 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# wp4-2024-starter +# GLITCH + +## Overzicht + +Dit project maakt gebruik van Docker-containers om meerdere toepassingen te orchestreren, waaronder een database, een Flask API-backend en React-gebaseerde web- en mobiele front-ends. Het heeft tot doel een naadloze ontwikkelings- en implementatieomgeving te bieden. + +## Belangrijke opmerkingen!! + +1. **URL-aanpassing in constante bestanden**: Zorg ervoor dat u de URL in de constantebestanden van zowel de web- als de mobiele frontend aanpast om overeen te komen met de URL van uw backend. + +2. **Problemen met inloggen en CORS**: Als u problemen ondervindt bij het inloggen, moet u mogelijk de CORS-instellingen (Cross-Origin Resource Sharing) op de backend aanpassen om het juiste domein toe te staan. + +3. **Gebruik van HTTPS**: Om HTTPS te gebruiken, moet u de "secure session" vlag inschakelen in uw Flask-applicatie en Flask-Talisman configureren om HTTPS af te dwingen. + +4. **Talisman en HTTPS**: Als u uw applicatie via HTTPS wilt laten draaien, moet u ervoor zorgen dat Flask-Talisman correct is geconfigureerd om HTTPS te handhaven. + +5. **Gebruik aub FireFox**: Gebruik alstublieft Firefox: Gebruik alstublieft Firefox voor testdoeleinden, omdat dit de enige browser is die onbeveiligde cookies ondersteunt. + +6. **Inloggen Gebruikgegevens**: +User Name: user1, Password: password1
+User Name: user2, Password: password2
+User Name: user3, Password: password3
+User Name: teacher1, Password: teacherpass
+ +## Projectstructuur + +- **database_**: Bevat bestanden die verband houden met de SQLite-databasecontainer. + - **Dockerfile**: Configuratie voor het uitvoeren van de SQLite-server. +- **backend**: Bevat de Flask API-backend. + - **Dockerfile**: Configuratie voor het uitvoeren python. +- **frontend**: + - **web**: Bevat de op React gebaseerde webfrontend. + - **Dockerfile**: Configuratie voor het uitvoeren python. + - **mobile**: Bevat de op React Native gebaseerde mobiele frontend. + - **Dockerfile**: Configuratie voor het uitvoeren python. + +## Installatie en Gebruik + +### Vereisten + +- Docker moet zijn geïnstalleerd op uw systeem. U kunt het downloaden van https://www.docker.com/get-started. + +### Het starten van de Containers + +1. Kloon deze repository naar uw lokale machine. + +2. Navigeer naar de projectdirectory. + +3. Gebruik Docker Compose om de containers te bouwen en uit te voeren: + + ```bash + docker-compose up --build + ``` + + Deze command zal alle containers bouwen en starten die zijn gedefinieerd in het `docker-compose.yml` bestand. + +### Toegang tot Toepassingen + +- **Flask API-backend**: + - Poort: 5000 + - Zodra de containers actief zijn, is de Flask API toegankelijk op `http://localhost:`. Vervang `` door de poort die is gespecificeerd in uw `docker-compose.yml`. + +- **React-webfrontend**: + - Poort: 3000 + - Nadat de containers zijn gestart, kunt u de webfrontend openen door naar `http://localhost:` te navigeren in uw webbrowser. Vervang `` door de poort die is gespecificeerd in uw `docker-compose.yml`. + +- **React Native mobiele frontend**: + - Poort: 8081 + - Om de React Native mobiele frontend uit te voeren, volgt u de instructies in de `mobile` map. U moet mogelijk aanvullende configuraties instellen voor uw ontwikkelomgeving. + +### Stoppen van de Containers + +Om de containers te stoppen, kunt u de volgende command gebruiken: + +```bash +docker-compose down diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..35d0796 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9 + +WORKDIR / + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..a590b41 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,194 @@ +import os +from flask import Flask, jsonify, request, session +from flask_cors import CORS +from flask_socketio import SocketIO, emit, join_room, leave_room +from models.user_model import UserTable +from models.domain_model import DomainTable +import schedule +import time +import threading +from routes.user_route import users_bp +from routes.domain_route import domains_bp +from flask_talisman import Talisman +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +from werkzeug.security import generate_password_hash, check_password_hash +from routes.validation import sanitize_input, is_valid_email + +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'fallback_secret_key') # Use environment variable +# app.config['SESSION_COOKIE_SECURE'] = True // https +# app.config['SESSION_COOKIE_HTTPONLY'] = True // http // https ssh +# app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + +limiter = Limiter( + app=app, + key_func=get_remote_address, + default_limits=["10 per minute"], + storage_uri="memory://" +) +allowed_origins = ["http://localhost:3000", "http://192.168.178.17:3000", "http://192.168.2.8:3000", "http://192.168.56.1:3000", "http://145.137.104.145:3000"] +socketio = SocketIO(app, cors_allowed_origins=allowed_origins) +CORS(app, origins=allowed_origins, supports_credentials=True) + +# talisman = Talisman( +# app, +# content_security_policy={ +# 'default-src': "'self'", +# 'script-src': "'self' 'unsafe-inline'", +# 'style-src': "'self' 'unsafe-inline'", +# 'img-src': "'self' data:", +# 'connect-src': "'self' ws: wss:", +# }, +# force_https=True, +# session_cookie_secure=True, +# strict_transport_security=True, +# referrer_policy='strict-origin-when-cross-origin' +# ) + +app.register_blueprint(users_bp) +app.register_blueprint(domains_bp) + +@app.route('/') +def main(): + return 'Welcome to your Flask-SocketIO application!' + +@socketio.on('connect') +def handle_connect(): + print('Client connected:', request.sid) + +@socketio.on('disconnect') +def handle_disconnect(): + print('Client disconnected:', request.sid) + +@socketio.on('join_user_room') +def handle_join_user_room(data): + room = str(sanitize_input(data.get("room", ""))) + if room: + join_room(room) + print(f'User {room} joined their room') + +@socketio.on('leave_room') +def handle_leave_room(data): + room = str(sanitize_input(data.get('room', ""))) + if room: + leave_room(room) + print(f'User left room: {room}') + +@app.route('/notify', methods=['POST']) +@limiter.limit("10/minute") +def notify(): + message = sanitize_input(request.json.get('message', 'Hello, World!')) + user_id = session.get('user_id') + if user_id: + socketio.emit('notification', {'message': message}, room=str(user_id)) + return jsonify({'status': 'notification sent'}) + else: + return jsonify({'status': 'user not authenticated'}), 401 + +@app.route('/send_message', methods=['POST']) +@limiter.limit("20/minute") +def send_message(): + data = request.json + user_id = sanitize_input(data.get('user_id', '')) + message = sanitize_input(data.get('message', '')) + + if user_id: + socketio.emit('new_message', {'message': message}, room=str(user_id)) + return jsonify({'status': 'message sent'}) + else: + return jsonify({'status': 'user_id not provided'}), 400 + +@app.route('/users/login', methods=['POST']) +@limiter.limit("5/minute") +def login(): + data = request.json + username = data.get('user_name', '') + password = data.get('password', '') + + if not username or not password: + return jsonify({'message': 'Username and password are required'}), 400 + + userTable = UserTable() + user = userTable.get_teacher_by_username(username) + + if user and check_password_hash(user['password'], password): + session['user_id'] = user['user_id'] + socketio.emit('join_user_room', room=str(user['user_id'])) + return jsonify({'message': 'User logged in successfully!', 'user': user}), 200 + + return jsonify({'message': 'Invalid username or password'}), 401 + +@app.route('/users/signup', methods=['POST']) +@limiter.limit("3/hour") +def signup(): + data = request.json + username = sanitize_input(data.get('user_name', '')) + password = data.get('password', '') + email = sanitize_input(data.get('email', '')) + first_name = sanitize_input(data.get('first_name', '')) + last_name = sanitize_input(data.get('last_name', '')) + + if not all([username, password, email, first_name, last_name]): + return jsonify({'message': 'All fields are required'}), 400 + + if not is_valid_email(email): + return jsonify({'message': 'Invalid email format'}), 400 + + userTable = UserTable() + if userTable.get_teacher_by_username(username): + return jsonify({'message': 'Username already exists'}), 409 + + hashed_password = generate_password_hash(password) + user = userTable.create_user( + username=username, + password=hashed_password, + email=email, + first_name=first_name, + last_name=last_name, + profile_picture=None, + IsTeacher=False + ) + + if user: + session['user_id'] = user['user_id'] + join_room(str(user['user_id'])) + return jsonify({'message': 'User signed up successfully!', 'user': user}), 201 + + return jsonify({'message': 'Something went wrong, please try again later'}), 500 + +@app.route('/users/check-auth', methods=['GET']) +def isAuth(): + return jsonify({'auth': 'user_id' in session}), 200 + +def check_messages(): + messages = DomainTable().get_unfinished_progress_modules() + for message in messages: + socketio.emit('notification', { + 'message': f'⚠️ Hurry Up! You have only {message["difference_in_days"]} days to finish 📚 {message["course_name"]} 🎓' + }, room=str(message['user_id'])) + +def schedule_checker(): + print(generate_password_hash('password1')) + print(generate_password_hash('password2')) + print(generate_password_hash('password3')) + print(generate_password_hash('teacherpass')) + while True: + schedule.run_pending() + time.sleep(1) + +if __name__ == '__main__': + socketio_thread = threading.Thread(target=socketio.run, args=(app,), kwargs={ + 'host': '0.0.0.0', + 'port': int(os.environ.get('PORT', 5000)), + # 'ssl_context': 'adhoc' # Use this for development. For production, use proper SSL certificates. + }) + socketio_thread.start() + + schedule.every(10).seconds.do(check_messages) + + schedule_thread = threading.Thread(target=schedule_checker) + schedule_thread.start() + + socketio_thread.join() + schedule_thread.join() \ No newline at end of file diff --git a/backend/database/createDatabase.py b/backend/database/createDatabase.py new file mode 100644 index 0000000..ec3166a --- /dev/null +++ b/backend/database/createDatabase.py @@ -0,0 +1,241 @@ +import sqlite3 + +# Establish connection to the database +connection = sqlite3.connect('database.db') + + +with open('./newglitch.sql', 'r') as script_file: + script = script_file.read() + + connection.executescript(script) + +connection.commit() + + + +connection.executescript(""" +--INSERT INTO domains (domain_name) VALUES +--('Software Engineering'), +--('Data Science'); + +--INSERT INTO courses (domain_id, course_name, description) VALUES +--(1, 'Introduction to Software Engineering', 'An introductory course to software engineering principles.'), +--(1, 'Software Design Patterns', 'Learn about common software design patterns.'), +--(1, 'Agile Software Development', 'An overview of agile methodologies in software development.'), +--(1, 'Software Testing and Quality Assurance', 'Explore testing techniques and quality assurance processes in software engineering.'), +--(2, 'Introduction to Data Science', 'An introductory course to data science principles.'), +--(2, 'Machine Learning', 'Learn about various machine learning algorithms and techniques.'), +--(2, 'Data Visualization', 'An overview of data visualization techniques.'), +--(2, 'Big Data Technologies', 'Explore big data technologies and their applications.'); + +INSERT INTO users (username, password, email, first_name, last_name, isTeacher) VALUES +('user1', 'pbkdf2:sha256:260000$mfTomSdhl3VXf0Rx$fda2ab9578474cc4778da15e10a8391aa35cf0146f7899ffc3917c3933bc90be', 'user1@example.com', 'John', 'Doe', 0), +('user2', 'pbkdf2:sha256:260000$xaskGZ11AncMeyja$5a4b73d5e30d0841a3c7c1f782b556777990cb23cea22b48f1b023aef0036b7e', 'user2@example.com', 'Jane', 'Smith', 0), +('user3', 'pbkdf2:sha256:260000$i031eWjjSgPBEcGO$f3dd69968de2567ed1cc2de1c199033a522ae44af9db3edaa511b2c9806bd79d', 'user3@example.com', 'Michael', 'Johnson', 0), +('teacher1', 'pbkdf2:sha256:260000$hr6GjEEfMsGzUWUc$f71e0e60e20a382b65e2323a24d9d2cacb742011768b2a632efd6078c89a0f39', 'teacher@example.com', 'Alice', 'Anderson', 1); + +-- Inserting instances +--INSERT INTO instances (course_id, instance_name, start_date, end_date) VALUES +-- Instances for Software Engineering Courses +--(1, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(1, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(1, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(1, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(2, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(2, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(2, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(2, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(3, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(3, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(3, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(3, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(4, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(4, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(4, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(4, 'Winter 2024', '2025-01-01', '2025-03-31'), +-- Instances for Data Science Courses +--(5, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(5, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(5, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(5, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(6, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(6, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(6, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(6, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(7, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(7, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(7, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(7, 'Winter 2024', '2025-01-01', '2025-03-31'), +--(8, 'Spring 2024', '2024-03-01', '2024-06-30'), +--(8, 'Summer 2024', '2024-07-01', '2024-09-30'), +--(8, 'Fall 2024', '2024-10-01', '2024-12-31'), +--(8, 'Winter 2024', '2025-01-01', '2025-03-31'); + +--INSERT INTO modules (instance_id, module_name, description, required_point) VALUES +-- Modules for Introduction to Software Engineering Instances +--(1, 'Module 1', 'Software Engineering Basics', 0), +--(1, 'Module 2', 'Software Engineering Practices', 50), +--(2, 'Module 3', 'Software Engineering Basics', 0), +--(2, 'Module 4', 'Software Engineering Practices', 50), +--(3, 'Module 5', 'Software Engineering Basics', 0), +--(3, 'Module 6', 'Software Engineering Practices', 50), +--(4, 'Module 7', 'Software Engineering Basics', 0), +--(4, 'Module 8', 'Software Engineering Practices', 50), +-- Modules for Software Design Patterns Instances +--(5, 'Module 1', 'Creational Patterns', 0), +--(5, 'Module 2', 'Structural Patterns', 60), +--(6, 'Module 3', 'Creational Patterns', 0), +--(6, 'Module 4', 'Structural Patterns', 60), +--(7, 'Module 5', 'Creational Patterns', 0), +--(7, 'Module 6', 'Structural Patterns', 60), +--(8, 'Module 7', 'Creational Patterns', 0), +--(8, 'Module 8', 'Structural Patterns', 60), +-- Modules for Agile Software Development Instances +--(9, 'Module 1', 'Agile Principles', 0), +--(9, 'Module 2', 'Agile Practices', 60), +--(10, 'Module 3', 'Agile Principles', 0), +--(10, 'Module 4', 'Agile Practices', 60), +--(11, 'Module 5', 'Agile Principles', 0), +--(11, 'Module 6', 'Agile Practices', 60), +--(12, 'Module 7', 'Agile Principles', 0), +--(12, 'Module 8', 'Agile Practices', 60), +-- Modules for Software Testing and Quality Assurance Instances +--(13, 'Module 1', 'Testing Basics', 0), +--(13, 'Module 2', 'Quality Assurance Techniques', 60), +--(14, 'Module 3', 'Testing Basics', 0), +--(14, 'Module 4', 'Quality Assurance Techniques', 60), +--(15, 'Module 5', 'Testing Basics', 0), +--(15, 'Module 6', 'Quality Assurance Techniques', 60), +--(16, 'Module 7', 'Testing Basics', 0), +--(16, 'Module 8', 'Quality Assurance Techniques', 60), +-- Modules for Introduction to Data Science Instances +--(17, 'Module 1', 'Data Science Basics', 0), +--(17, 'Module 2', 'Data Science Tools', 50), +--(18, 'Module 3', 'Data Science Basics', 0), +--(18, 'Module 4', 'Data Science Tools', 50), +--(19, 'Module 5', 'Data Science Basics', 0), +--(19, 'Module 6', 'Data Science Tools', 50), +--(20, 'Module 7', 'Data Science Basics', 0), +--(20, 'Module 8', 'Data Science Tools', 50), +-- Modules for Machine Learning Instances +--(21, 'Module 1', 'Supervised Learning', 0), +--(21, 'Module 2', 'Unsupervised Learning', 70), +--(22, 'Module 3', 'Supervised Learning', 0), +--(22, 'Module 4', 'Unsupervised Learning', 70), +--(23, 'Module 5', 'Supervised Learning', 0), +--(23, 'Module 6', 'Unsupervised Learning', 70), +--(24, 'Module 7', 'Supervised Learning', 0), +--(24, 'Module 8', 'Unsupervised Learning', 70), +-- Modules for Data Visualization Instances +--(25, 'Module 1', 'Visualization Principles', 0), +--(25, 'Module 2', 'Visualization Tools', 60), +--(26, 'Module 3', 'Visualization Principles', 0), +--(26, 'Module 4', 'Visualization Tools', 60), +--(27, 'Module 5', 'Visualization Principles', 0), +--(27, 'Module 6', 'Visualization Tools', 60), +--(28, 'Module 7', 'Visualization Principles', 0), +--(28, 'Module 8', 'Visualization Tools', 60), +-- Modules for Big Data Technologies Instances +--(29, 'Module 1', 'Big Data Fundamentals', 0), +--(29, 'Module 2', 'Big Data Applications', 80), +--(30, 'Module 3', 'Big Data Fundamentals', 0), +--(30, 'Module 4', 'Big Data Applications', 80), +--(31, 'Module 5', 'Big Data Fundamentals', 0), +--(31, 'Module 6', 'Big Data Applications', 80), +--(32, 'Module 7', 'Big Data Fundamentals', 0), +--(32, 'Module 8', 'Big Data Applications', 80); + +--INSERT INTO activities (module_id, activity_name, description, level, type, point) VALUES +-- Activities for Introduction to Software Engineering Modules +--(1, 'Assignment 1', 'Software Engineering Basics Assignment', 1, 'Task', 40), +--(1, 'Quiz 1', 'Software Engineering Basics Quiz', 1, 'Challenge', 60), +--(1, 'Project 1', 'Software Engineering Basics Project', 1, 'Project', 100), +--(2, 'Assignment 2', 'Software Engineering Practices Assignment', 2, 'Task', 50), +--(2, 'Quiz 2', 'Software Engineering Practices Quiz', 2, 'Challenge', 70), +--(2, 'Project 2', 'Software Engineering Practices Project', 2, 'Project', 120), +-- Add similar sets of activities for each module, making sure they are specific to the course and specialization. +-- Activities for Software Design Patterns Modules +--(5, 'Assignment 1', 'Creational Patterns Assignment', 1, 'Task', 40), +--(5, 'Quiz 1', 'Creational Patterns Quiz', 1, 'Challenge', 60), +--(5, 'Project 1', 'Creational Patterns Project', 1, 'Project', 100), +--(6, 'Assignment 2', 'Structural Patterns Assignment', 2, 'Task', 60), +--(6, 'Quiz 2', 'Structural Patterns Quiz', 2, 'Challenge', 80), +--(6, 'Project 2', 'Structural Patterns Project', 2, 'Project', 140), +-- Activities for Agile Software Development Modules +--(9, 'Assignment 1', 'Agile Principles Assignment', 1, 'Task', 40), +--(9, 'Quiz 1', 'Agile Principles Quiz', 1, 'Challenge', 60), +--(9, 'Project 1', 'Agile Principles Project', 1, 'Project', 100), +--(10, 'Assignment 2', 'Agile Practices Assignment', 2, 'Task', 60), +--(10, 'Quiz 2', 'Agile Practices Quiz', 2, 'Challenge', 80), +--(10, 'Project 2', 'Agile Practices Project', 2, 'Project', 140), +-- Activities for Software Testing and Quality Assurance Modules +--(13, 'Assignment 1', 'Testing Basics Assignment', 1, 'Task', 40), +--(13, 'Quiz 1', 'Testing Basics Quiz', 1, 'Challenge', 60), +--(13, 'Project 1', 'Testing Basics Project', 1, 'Project', 100), +--(14, 'Assignment 2', 'Quality Assurance Techniques Assignment', 2, 'Task', 60), +--(14, 'Quiz 2', 'Quality Assurance Techniques Quiz', 2, 'Challenge', 80), +--(14, 'Project 2', 'Quality Assurance Techniques Project', 2, 'Project', 140), +-- Activities for Introduction to Data Science Modules +--(17, 'Assignment 1', 'Data Science Basics Assignment', 1, 'Task', 40), +--(17, 'Quiz 1', 'Data Science Basics Quiz', 1, 'Challenge', 60), +--(17, 'Project 1', 'Data Science Basics Project', 1, 'Project', 100), +--(18, 'Assignment 2', 'Data Science Tools Assignment', 2, 'Task', 50), +--(18, 'Quiz 2', 'Data Science Tools Quiz', 2, 'Challenge', 70), +--(18, 'Project 2', 'Data Science Tools Project', 2, 'Project', 120), +-- Activities for Machine Learning Modules +--(21, 'Assignment 1', 'Supervised Learning Assignment', 1, 'Task', 50), +--(21, 'Quiz 1', 'Supervised Learning Quiz', 1, 'Challenge', 70), +--(21, 'Project 1', 'Supervised Learning Project', 1, 'Project', 120), +--(22, 'Assignment 2', 'Unsupervised Learning Assignment', 2, 'Task', 70), +--(22, 'Quiz 2', 'Unsupervised Learning Quiz', 2, 'Challenge', 90), +--(22, 'Project 2', 'Unsupervised Learning Project', 2, 'Project', 140), +---- Activities for Data Visualization Modules +--(25, 'Assignment 1', 'Visualization Principles Assignment', 1, 'Task', 40), +--(25, 'Quiz 1', 'Visualization Principles Quiz', 1, 'Challenge', 60), +--(25, 'Project 1', 'Visualization Principles Project', 1, 'Project', 100), +--(26, 'Assignment 2', 'Visualization Tools Assignment', 2, 'Task', 60), +--(26, 'Quiz 2', 'Visualization Tools Quiz', 2, 'Challenge', 80), +--(26, 'Project 2', 'Visualization Tools Project', 2, 'Project', 140), +-- Activities for Big Data Technologies Modules +--(29, 'Assignment 1', 'Big Data Fundamentals Assignment', 1, 'Task', 50), +--(29, 'Quiz 1', 'Big Data Fundamentals Quiz', 1, 'Challenge', 70), +--(29, 'Project 1', 'Big Data Fundamentals Project', 1, 'Project', 120), +--(30, 'Assignment 2', 'Big Data Applications Assignment', 2, 'Task', 70), +--(30, 'Quiz 2', 'Big Data Applications Quiz', 2, 'Challenge', 90), +--(30, 'Project 2', 'Big Data Applications Project', 2, 'Project', 140); + +--INSERT INTO usermodules (user_id, module_id) VALUES +--(1, 1), +--(2, 2), +--(3, 3), +--(4, 4); + +-- Inserting userprogressmodules +--INSERT INTO userprogressmodules (user_activity_id, reviwed_by, submited, finished, passed) VALUES +--(1, 4, 1, 1, 1), +--(2, 4, 1, 1, 1), +--(3, 4, 1, 1, 1), +--(4, 4, 1, 1, 1), +--(5, 4, 1, 1, 1), +--(6, 4, 1, 1, 1), +--(7, 4, 1, 1, 1), +--(8, 4, 1, 1, 1); + +--INSERT INTO userposts (user_progress_module_id, content) VALUES +--(1, 'Completed Assignment 1. It was challenging but insightful.'), +--(2, 'Completed Quiz 1. Found some questions tricky.'), +--(3, 'Assignment 2 submitted. Ready for the next challenge.'), +--(4, 'Quiz 2 completed. Feeling confident with design patterns.'); + +--INSERT INTO userpostcomments (user_id, user_post_id, comment) VALUES +--(2, 1, '👍'), +--(3, 2, '👍'), +--(4, 3, '🎉'), +--(1, 4, '❤️'); +--""") + +# Commit changes +connection.commit() + +# Close the connection +connection.close() diff --git a/backend/database/database.db b/backend/database/database.db new file mode 100644 index 0000000..b8eb356 Binary files /dev/null and b/backend/database/database.db differ diff --git a/backend/database/glitch.sql b/backend/database/glitch.sql new file mode 100644 index 0000000..00fd412 --- /dev/null +++ b/backend/database/glitch.sql @@ -0,0 +1,103 @@ +CREATE TABLE users ( + "user_id" integer PRIMARY KEY AUTOINCREMENT, + "username" TEXT , + "password" TEXT , + "email" text , + "first_name" TEXT , + "last_name" TEXT , + "profile_picture" TEXT , + "IsTeacher" integer , + "registration_date" timestamp DEFAULT CURRENT_TIMESTAMP +) ; + + + +CREATE TABLE domains ( + "domain_id" integer PRIMARY KEY AUTOINCREMENT, + "domain_name" text +) ; + + +CREATE TABLE courses ( + "course_id" integer PRIMARY KEY AUTOINCREMENT, + "domain_id" integer , + "course_name" text , + "description" text , + FOREIGN KEY("domain_id") REFERENCES domains("domain_id") +) ; + + +CREATE TABLE instances ( + "instance_id" integer PRIMARY KEY AUTOINCREMENT, + "course_id" integer , + "instance_name" text , + "start_date" DATETIME , + "end_date" DATETIME , + FOREIGN KEY("course_id") REFERENCES courses("course_id") + +) ; + + + +CREATE TABLE modules ( + "module_id" integer PRIMARY KEY AUTOINCREMENT, + "instance_id" integer , + "module_name" text , + "description" text , + "required_point" integer , + FOREIGN KEY("instance_id") REFERENCES instances("instance_id") +) ; + + + +CREATE TABLE activities ( + "activity_id" integer PRIMARY KEY AUTOINCREMENT, + "module_id" integer , + "activity_name" text , + "description" text , + "level" integer , + "type" text , + "point" integer , + FOREIGN KEY("module_id") REFERENCES modules("module_id") +) ; + + + +CREATE TABLE usermodules ( + "user_module_id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" integer , + "module_id" integer , + FOREIGN KEY("user_id") REFERENCES users("user_id"), + FOREIGN KEY("module_id") REFERENCES modules("module_id") +) ; + +CREATE TABLE userprogressmodules ( + "user_progress_module_id" integer PRIMARY KEY AUTOINCREMENT, + "user_activity_id" integer , + "reviwed_by" integer , + "submited" integer , + "finished" integer , + "passed" integer , + FOREIGN KEY("reviwed_by") REFERENCES users("user_id"), + FOREIGN KEY("user_activity_id") REFERENCES activities("activity_id") +) ; + + +CREATE TABLE userposts ( + "user_post_id" integer PRIMARY KEY AUTOINCREMENT, + "user_progress_module_id" integer , + "content" text , + FOREIGN KEY("user_progress_module_id") REFERENCES userprogressmodules("user_progress_module_id") + +) ; + + +CREATE TABLE userpostcomments ( + "user_post_comment_id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" integer , + "user_post_id" integer , + "comment" text , + FOREIGN KEY("user_id") REFERENCES users("user_id"), + FOREIGN KEY("user_post_id") REFERENCES userposts("user_post_id") +) ; + diff --git a/backend/database/newglitch.sql b/backend/database/newglitch.sql new file mode 100644 index 0000000..339608c --- /dev/null +++ b/backend/database/newglitch.sql @@ -0,0 +1,104 @@ +CREATE TABLE users ( + "user_id" integer PRIMARY KEY AUTOINCREMENT, + "username" TEXT , + "password" TEXT , + "email" text , + "first_name" TEXT , + "last_name" TEXT , + "profile_picture" TEXT , + "isTeacher" integer , + "registration_date" timestamp DEFAULT CURRENT_TIMESTAMP +) ; + + + +CREATE TABLE domains ( + "domain_id" integer PRIMARY KEY AUTOINCREMENT, + "domain_name" text +) ; + + +CREATE TABLE courses ( + "course_id" integer PRIMARY KEY AUTOINCREMENT, + "domain_id" integer , + "course_name" text , + "description" text , + FOREIGN KEY("domain_id") REFERENCES domains("domain_id") +) ; + + +CREATE TABLE instances ( + "instance_id" integer PRIMARY KEY AUTOINCREMENT, + "course_id" integer , + "instance_name" text , + "start_date" DATETIME , + "end_date" DATETIME , + FOREIGN KEY("course_id") REFERENCES courses("course_id") + +) ; + + + +CREATE TABLE modules ( + "module_id" integer PRIMARY KEY AUTOINCREMENT, + "instance_id" integer , + "module_name" text , + "description" text , + "required_point" integer , + FOREIGN KEY("instance_id") REFERENCES instances("instance_id") +) ; + + + +CREATE TABLE activities ( + "activity_id" integer PRIMARY KEY AUTOINCREMENT, + "module_id" integer , + "activity_name" text , + "description" text , + "level" integer , + "type" text , + "point" integer , + FOREIGN KEY("module_id") REFERENCES modules("module_id") +) ; + + + +CREATE TABLE usermodules ( + "user_module_id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" integer , + "module_id" integer , + FOREIGN KEY("user_id") REFERENCES users("user_id"), + FOREIGN KEY("module_id") REFERENCES modules("module_id") +) ; + +CREATE TABLE userprogressmodules ( + "user_progress_module_id" integer PRIMARY KEY AUTOINCREMENT, + "user_activity_id" integer , + "reviwed_by" integer , + "submited" integer , + "finished" integer , + "passed" integer , + FOREIGN KEY("reviwed_by") REFERENCES users("user_id"), + FOREIGN KEY("user_activity_id") REFERENCES activities("activity_id") +) ; + + +CREATE TABLE userposts ( + "user_post_id" integer PRIMARY KEY AUTOINCREMENT, + "user_progress_module_id" integer , + "content" text , + FOREIGN KEY("user_progress_module_id") REFERENCES userprogressmodules("user_progress_module_id") + +) ; + + +CREATE TABLE userpostcomments ( + "user_post_comment_id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" integer , + "user_post_id" integer , + "comment" text , + FOREIGN KEY("user_id") REFERENCES users("user_id"), + FOREIGN KEY("user_post_id") REFERENCES userposts("user_post_id") +) ; + + diff --git a/backend/models/domain_model.py b/backend/models/domain_model.py new file mode 100644 index 0000000..4a2afae --- /dev/null +++ b/backend/models/domain_model.py @@ -0,0 +1,767 @@ +import sqlite3 +from collections import defaultdict +from datetime import datetime + +class DomainTable: + def __init__(self, db_path='database/database.db'): + self.db_path = db_path + def _connect(self): + return sqlite3.connect(self.db_path) + + def create_domain(self,domain_name ): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO domains (domain_name) VALUES (?)""", (domain_name,)) + conn.commit() + domain_id = cursor.lastrowid + conn.close() + return dict({"domain_id": domain_id ,"domain_name":domain_name}) + + def create_course(self,domain_id,course_name,description ): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO courses (domain_id,course_name,description) VALUES (?,?,?)""", (domain_id,course_name,description,)) + conn.commit() + course_id = cursor.lastrowid + cursor.execute("""SELECT * FROM courses WHERE course_id = ?""", (course_id,)) + course = cursor.fetchone() + conn.close() + return dict(course) + + def get_analytics_data(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + users = conn.execute('SELECT * FROM users').fetchall() + courses = conn.execute('SELECT * FROM courses').fetchall() + instances = conn.execute('SELECT * FROM instances').fetchall() + modules = conn.execute('SELECT * FROM modules').fetchall() + activities = conn.execute('SELECT * FROM activities').fetchall() + user_modules = conn.execute('SELECT * FROM usermodules').fetchall() + user_progress_modules = conn.execute('SELECT * FROM userprogressmodules').fetchall() + conn.close() + return { + 'users': [dict(user) for user in users], + 'courses': [dict(course) for course in courses], + 'instances': [dict(instance) for instance in instances], + 'modules': [dict(module) for module in modules], + 'activities': [dict(activity) for activity in activities], + 'user_modules': [dict(user_module) for user_module in user_modules], + 'user_progress_modules': [dict(user_progress_module) for user_progress_module in user_progress_modules], + } + + + def get_modules_by_instance_id(self, instance_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(""" + SELECT + m.module_id, + m.module_name, + m.description AS module_description, + m.required_point, + i.instance_id, + i.instance_name, + i.start_date, + i.end_date, + i.course_id, + c.course_name, + c.description AS course_description + FROM + modules m + LEFT JOIN + instances i ON m.instance_id = i.instance_id + LEFT JOIN + courses c ON i.course_id = c.course_id + WHERE + i.instance_id = ? +""", (instance_id,)) + modules = cursor.fetchall() + return [dict(module) for module in modules] + + def get_activities_by_module_id(self, module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM activities WHERE module_id = ? """, (module_id,)) + activities = cursor.fetchall() + return [dict(activity) for activity in activities] + + def get_instances_by_course_id(self, course_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT + i.instance_id, + i.instance_name, + i.start_date, + i.end_date, + c.course_id, + c.course_name, + c.description AS course_description + FROM + instances i + LEFT JOIN + courses c ON i.course_id = c.course_id + WHERE + c.course_id = ? """, (course_id,)) + instances = cursor.fetchall() + return [dict(instance) for instance in instances] + + + def create_usermodules(self,user_id,module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO usermodules (user_id,module_id) VALUES (?,?)""", (user_id,module_id,)) + conn.commit() + conn.close() + + def create_userprogressmodule(self, user_activity_id, reviwed_by=None, submitted=0, finished=0, passed=0): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO userprogressmodules (user_activity_id, reviwed_by, submited, finished, passed) + VALUES (?, ?, ?, ?, ?) + """, + (user_activity_id, reviwed_by, submitted, finished, passed) + ) + conn.commit() + conn.close() + + + def register_usermodules(self,user_id,module_id): + self.create_usermodules(user_id,module_id) + activities = self.get_activities_by_module_id(module_id) + for activity in activities: + activity_id = activity["activity_id"] + self.create_userprogressmodule(user_activity_id=activity_id) + + def get_userprogressmodules_by_module_id(self,module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = ''' + SELECT + upm.user_progress_module_id, + upm.user_activity_id, + upm.reviwed_by, + upm.submited, + upm.finished, + upm.passed, + a.activity_name, + a.description AS activity_description, + a.level, + a.type, + a.point, + m.module_name, + m.module_id, + m.description AS module_description, + i.instance_id, + i.instance_name, + c.course_id, + c.course_name, + c.description AS course_description, + d.domain_id, + d.domain_name, + u.first_name, + u.last_name, + up.user_post_id, + up.content AS post_content +FROM + userprogressmodules upm +JOIN + activities a ON upm.user_activity_id = a.activity_id +JOIN + modules m ON a.module_id = m.module_id +JOIN + instances i ON i.instance_id = m.instance_id +JOIN + courses c ON c.course_id = i.course_id +JOIN + domains d ON d.domain_id = c.domain_id +LEFT JOIN + users u ON upm.reviwed_by = u.user_id +LEFT JOIN + userposts up ON upm.user_progress_module_id = up.user_progress_module_id +WHERE + m.module_id = ? + + + ''' + cursor.execute(query,(module_id,)) + modules = cursor.fetchall() + return [dict(module) for module in modules] + + + def create_instance(self,course_id,instance_name,start_date,end_date): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO instances (course_id,instance_name,start_date,end_date) VALUES (?,?,?,?)""", (course_id,instance_name,start_date,end_date,)) + conn.commit() + instance_id = cursor.lastrowid + cursor.execute("""SELECT * FROM instances WHERE instance_id = ?""", (instance_id,)) + instance = cursor.fetchone() + conn.close() + return dict(instance) + + def create_modules(self,instance_id,module_name,description,required_point): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO modules (instance_id,module_name,description,required_point) VALUES (?,?,?,?)""", (instance_id,module_name,description,required_point,)) + conn.commit() + module_id = cursor.lastrowid + cursor.execute("""SELECT * FROM modules WHERE module_id = ?""", (module_id,)) + module = cursor.fetchone() + conn.close() + return dict(module) + + + def create_activities(self, module_id, activity_name, description, level, activity_type, point): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + try: + cursor.execute(""" + INSERT INTO activities (module_id, activity_name, description, level, type, point) + VALUES (?, ?, ?, ?, ?, ?) + """, (module_id, activity_name, description, level, activity_type, point)) + conn.commit() + activity_id = cursor.lastrowid + cursor.execute(""" + SELECT user_id FROM usermodules WHERE module_id = ? + """, (module_id,)) + user_ids = cursor.fetchall() + for user_id_tuple in user_ids: + user_id = user_id_tuple['user_id'] + cursor.execute(""" + INSERT INTO userprogressmodules (user_activity_id, reviwed_by, submited, finished, passed) + VALUES (?, NULL, 0, 0, 0) + """, (activity_id,)) + + conn.commit() + cursor.execute(""" + SELECT * FROM activities WHERE activity_id = ? + """, (activity_id,)) + activity = cursor.fetchone() + except sqlite3.Error as e: + conn.rollback() + return None + finally: + conn.close() + return dict(activity) + + + + def get_all_domains_joined_with_courses(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + SELECT + d.domain_id, + d.domain_name, + c.course_id, + c.course_name, + c.description AS course_description, + i.instance_id, + i.instance_name, + i.start_date AS instance_start_date, + i.end_date AS instance_end_date, + m.module_id, + m.module_name, + m.description AS module_description, + m.required_point, + a.activity_id, + a.activity_name, + a.description AS activity_description, + a.level, + a.type, + a.point + FROM + domains d + LEFT JOIN + courses c ON d.domain_id = c.domain_id + LEFT JOIN + instances i ON c.course_id = i.course_id + LEFT JOIN + modules m ON i.instance_id = m.instance_id + LEFT JOIN + activities a ON m.module_id = a.module_id + ORDER BY + d.domain_id, c.course_id, i.instance_id, m.module_id, a.activity_id; + """ + + cursor.execute(query) + rows = cursor.fetchall() + + domains_dict = defaultdict(lambda: {"domain_id": None, "domain_name": None, "courses": []}) + + for row in rows: + domain_id = row["domain_id"] + course_id = row["course_id"] + instance_id = row["instance_id"] + module_id = row["module_id"] + activity_id = row["activity_id"] + + if domains_dict[domain_id]["domain_id"] is None: + domains_dict[domain_id]["domain_id"] = domain_id + domains_dict[domain_id]["domain_name"] = row["domain_name"] + + courses = domains_dict[domain_id]["courses"] + course = next((c for c in courses if c["course_id"] == course_id), None) + if not course: + course = { + "course_id": course_id, + "course_name": row["course_name"], + "course_description": row["course_description"], + "instances": [] + } + courses.append(course) + + instances = course["instances"] + instance = next((i for i in instances if i["instance_id"] == instance_id), None) + if not instance: + instance = { + "instance_id": instance_id, + "instance_name": row["instance_name"], + "instance_start_date": row["instance_start_date"], + "instance_end_date": row["instance_end_date"], + "modules": [] + } + instances.append(instance) + + modules = instance["modules"] + module = next((m for m in modules if m["module_id"] == module_id), None) + if not module: + module = { + "module_id": module_id, + "module_name": row["module_name"], + "module_description": row["module_description"], + "required_point": row["required_point"], + "activities": [] + } + modules.append(module) + + activities = module["activities"] + activity = next((a for a in activities if a["activity_id"] == activity_id), None) + if not activity: + activity = { + "activity_id": activity_id, + "activity_name": row["activity_name"], + "activity_description": row["activity_description"], + "level": row["level"], + "type": row["type"], + "point": row["point"] + } + activities.append(activity) + + all_domains = list(domains_dict.values()) + + return all_domains + + + def get_registered_domain_by_usermodules(self, user_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(""" + SELECT domains.*, courses.*, modules.*, activities.* + FROM usermodules + JOIN modules ON usermodules.module_id = modules.module_id + JOIN instances ON modules.instance_id = instances.instance_id + JOIN courses ON instances.course_id = courses.course_id + JOIN domains ON courses.domain_id = domains.domain_id + JOIN activities ON modules.module_id = activities.module_id + WHERE usermodules.user_id = ? + """, (user_id,)) + result = cursor.fetchone() + conn.close() + return result + + + def get_all_courses(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM courses""") + courses = cursor.fetchall() + conn.close() + all_courses = [dict(course) for course in courses] + return all_courses + + + def register_user_in_module(self, user_id, module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO usermodules (user_id, module_id) VALUES (?, ?)""", (user_id, module_id)) + + cursor.execute(""" + SELECT activity_id + FROM activities + WHERE module_id = ? + """, (module_id,)) + activities = cursor.fetchall() + + for activity in activities: + activity_id = activity['activity_id'] + cursor.execute(""" + INSERT INTO userprogressmodules (user_activity_id, reviwed_by, submited, finished, passed) + VALUES (?, NULL, 0, 0, 0) + """, (activity_id,)) + + conn.commit() + conn.close() + + def submit_user_progress_module_id(self,user_progress_module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + UPDATE userprogressmodules + SET submited = 1, finished = 1 + WHERE user_progress_module_id = ? + """ + cursor.execute(query, (user_progress_module_id,)) + conn.commit() + conn.close() + + def mark_user_progress_module_as_failed(self,reviwed_by,user_progress_module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + UPDATE userprogressmodules + SET passed = 0, reviwed_by = ? + WHERE user_progress_module_id = ? + """ + cursor.execute(query, (reviwed_by,user_progress_module_id,)) + conn.commit() + conn.close() + + def mark_user_progress_module_as_passed(self,reviwed_by,user_progress_module_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + UPDATE userprogressmodules + SET passed = 1, reviwed_by = ? + WHERE user_progress_module_id = ? + """ + cursor.execute(query, (reviwed_by,user_progress_module_id,)) + conn.commit() + conn.close() + + + def get_requested_tasks(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query= """SELECT upm.user_progress_module_id, upm.user_activity_id, upm.reviwed_by, upm.submited, upm.finished, upm.passed, + a.activity_name, a.description AS activity_description, a.level AS activity_level, a.type AS activity_type, a.point AS activity_point, + m.module_id, m.module_name, m.description AS module_description, m.required_point, + i.instance_id, i.instance_name, i.start_date, i.end_date, + c.course_id, c.course_name, c.description AS course_description, + u.user_id, u.username, u.password, u.email, u.first_name, u.last_name, u.profile_picture, u.IsTeacher, u.registration_date + FROM userprogressmodules upm + JOIN activities a ON upm.user_activity_id = a.activity_id + JOIN modules m ON a.module_id = m.module_id + JOIN instances i ON m.instance_id = i.instance_id + JOIN courses c ON i.course_id = c.course_id + JOIN usermodules um ON m.module_id = um.module_id + JOIN users u ON um.user_id = u.user_id + WHERE upm.submited = 1; + """ + cursor.execute(query) + requests = cursor.fetchall() + conn.close() + return [dict(req) for req in requests] + + def get_registered_modules(self,user_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = ''' + SELECT + domains.domain_name, + courses.course_name, + modules.module_name, + modules.module_id, + activities.activity_name, + activities.point, + userprogressmodules.passed, + userprogressmodules.reviwed_by + + + FROM + userprogressmodules + JOIN + activities ON userprogressmodules.user_activity_id = activities.activity_id + JOIN + modules ON activities.module_id = modules.module_id + JOIN + instances ON modules.instance_id = instances.instance_id + JOIN + courses ON instances.course_id = courses.course_id + JOIN + domains ON courses.domain_id = domains.domain_id + JOIN + usermodules ON modules.module_id = usermodules.module_id + JOIN + users ON usermodules.user_id = users.user_id + WHERE + users.user_id = ? + ''' + + cursor.execute(query, (user_id,)) + results = cursor.fetchall() + conn.close() + return [dict(req) for req in results] + + + def create_user_progress_post(self,user_progress_module_id,content): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO userposts (user_progress_module_id, content) VALUES (?, ?)""", (user_progress_module_id,content)) + conn.commit() + conn.close() + + def create_userpostcomments(self,user_id,user_post_id,comment): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""INSERT INTO userpostcomments (user_id,user_post_id,comment) VALUES (?, ?, ?)""", (user_id,user_post_id,comment)) + conn.commit() + conn.close() + + + + def get_all_user_posts(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """SELECT + userposts.user_post_id , + userposts.content, + users.user_id, + users.first_name, + users.last_name + FROM + userposts + JOIN + userprogressmodules ON userposts.user_progress_module_id = userprogressmodules.user_progress_module_id + JOIN + users ON userprogressmodules.reviwed_by = users.user_id + """ + cursor.execute(query) + results = cursor.fetchall() + conn.close() + return [dict(post) for post in results] + + + def get_all_user_postscomments(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """SELECT + userpostcomments.user_post_comment_id , + userpostcomments.user_id, + userpostcomments.user_post_id , + userpostcomments.comment, + users.user_id, + users.first_name, + users.last_name + FROM + userpostcomments + JOIN + users ON userpostcomments.user_id = users.user_id + """ + cursor.execute(query) + results = cursor.fetchall() + conn.close() + return [dict(post) for post in results] + + def get_userpost_with_comments(self): + all_posts = self.get_all_user_posts() + all_comments = self.get_all_user_postscomments() + + post_comments_dict = {} + + for post in all_posts: + post_id = post['user_post_id'] + post_comments_dict[post_id] = { + 'post': post, + 'comments': {} + } + + for comment in all_comments: + post_id = comment['user_post_id'] + emoji = comment['comment'] + user_name = f"{comment['first_name']} {comment['last_name']}" + if post_id in post_comments_dict: + if emoji not in post_comments_dict[post_id]['comments']: + post_comments_dict[post_id]['comments'][emoji] = {'count': 0, 'users': []} + post_comments_dict[post_id]['comments'][emoji]['count'] += 1 + post_comments_dict[post_id]['comments'][emoji]['users'].append(user_name) + + grouped_posts = list(post_comments_dict.values()) + + return grouped_posts + + def get_unfinished_progress_modules(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + SELECT u.user_id, c.course_name, i.end_date, i.instance_name , m.module_id, upm.finished + FROM users u + JOIN usermodules um ON u.user_id = um.user_id + JOIN modules m ON um.module_id = m.module_id + JOIN instances i ON m.instance_id = i.instance_id + JOIN courses c ON c.course_id = i.course_id + LEFT JOIN activities a ON m.module_id = a.module_id + LEFT JOIN userprogressmodules upm ON a.activity_id = upm.user_activity_id + """ + cursor.execute(query) + rows = cursor.fetchall() + user_progress = {} + messeges = [] + current_date = datetime.now() + for row in rows: + user_id,course_name, end_date, instance_name, module_id, finished = row + if user_id not in user_progress: + user_progress[user_id] = { + 'end_date': datetime.strptime(end_date, '%Y-%m-%d'), + 'instance_name': instance_name, + 'course_name': course_name, + 'modules': {} + } + if module_id not in user_progress[user_id]['modules']: + user_progress[user_id]['modules'][module_id] = True + + if not finished: + user_progress[user_id]['modules'][module_id] = False + + for user_id, progress in user_progress.items(): + instance_name = progress['instance_name'] + course_name = progress['course_name'] + difference = progress['end_date'] - current_date + difference_in_days = difference.days + if difference_in_days < 15 : + messeges.append({"instance_name": instance_name,"course_name" : course_name ,"difference_in_days" :difference_in_days, "end_date":progress['end_date'], "user_id": user_id }) + conn.close() + return messeges + + + def get_all_domaines_with_realted_data(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + query = """ + SELECT + d.domain_id, + d.domain_name, + c.course_id, + c.course_name, + c.description AS course_description, + i.instance_id, + i.instance_name, + i.start_date, + i.end_date, + m.module_id, + m.module_name, + m.description AS module_description, + m.required_point, + a.activity_id, + a.activity_name, + a.description AS activity_description, + a.level, + a.type, + a.point + FROM + domains d + LEFT JOIN + courses c ON d.domain_id = c.domain_id + LEFT JOIN + instances i ON c.course_id = i.course_id + LEFT JOIN + modules m ON i.instance_id = m.instance_id + LEFT JOIN + activities a ON m.module_id = a.module_id + ORDER BY + d.domain_id, c.course_id, i.instance_id, m.module_id, a.activity_id; + """ + cursor.execute(query) + rows = cursor.fetchall() + conn.close() + domains_dict = defaultdict(lambda: {"domain_id": None, "domain_name": None, "courses": []}) + + for row in rows: + domain_id = row["domain_id"] + course_id = row["course_id"] + instance_id = row["instance_id"] + module_id = row["module_id"] + activity_id = row["activity_id"] + + if domains_dict[domain_id]["domain_id"] is None: + domains_dict[domain_id]["domain_id"] = domain_id + domains_dict[domain_id]["domain_name"] = row["domain_name"] + + courses = domains_dict[domain_id]["courses"] + course = next((c for c in courses if c["course_id"] == course_id), None) + if not course: + course = { + "course_id": course_id, + "course_name": row["course_name"], + "course_description": row["course_description"], + "instances": [] + } + courses.append(course) + + instances = course["instances"] + instance = next((i for i in instances if i["instance_id"] == instance_id), None) + if not instance: + instance = { + "instance_id": instance_id, + "instance_name": row["instance_name"], + "instance_start_date": row["start_date"], + "instance_end_date": row["end_date"], + "modules": [] + } + instances.append(instance) + + modules = instance["modules"] + module = next((m for m in modules if m["module_id"] == module_id), None) + if not module: + module = { + "module_id": module_id, + "module_name": row["module_name"], + "module_description": row["module_description"], + "required_point": row["required_point"], + "activities": [] + } + modules.append(module) + + activities = module["activities"] + activity = next((a for a in activities if a["activity_id"] == activity_id), None) + if not activity: + activity = { + "activity_id": activity_id, + "activity_name": row["activity_name"], + "activity_description": row["activity_description"], + "level": row["level"], + "type": row["type"], + "point": row["point"] + } + activities.append(activity) + + all_domains = list(domains_dict.values()) + return all_domains diff --git a/backend/models/user_model.py b/backend/models/user_model.py new file mode 100644 index 0000000..04d79bb --- /dev/null +++ b/backend/models/user_model.py @@ -0,0 +1,98 @@ +import sqlite3 + +class UserTable: + def __init__(self, db_path='database/database.db'): + self.db_path = db_path + def _connect(self): + return sqlite3.connect(self.db_path) + + def create_user (self,username, password, email, first_name, last_name, profile_picture = None, IsTeacher = 0): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (username, password, email, first_name, last_name, profile_picture, IsTeacher) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (username, password, email, first_name, last_name, profile_picture, IsTeacher)) + conn.commit() + user_id = cursor.lastrowid + cursor.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)) + user = cursor.fetchone() + conn.close() + return dict(user) + + # remove unnecessary functions + + # def get_teacher_by_username_password(self, username, password): + # conn = self._connect() + # conn.row_factory = sqlite3.Row + # cursor = conn.cursor() + # cursor.execute("""SELECT * FROM users WHERE username = ? AND password = ?""", (username, password)) + # user = cursor.fetchone() + # conn.close() + # dicUser = dict(user) if user else None + # return dicUser + + def get_teacher_by_username(self, username): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM users WHERE username = ?""", (username,)) + user = cursor.fetchone() + conn.close() + dicUser = dict(user) if user else None + return dicUser + + def get_teacher_by_email_password(self, email, password): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM users WHERE email = ? AND password = ?""", (email, password)) + user = cursor.fetchone() + conn.close() + dicUser = dict(user) if user else None + return dicUser + + def get_user_by_id(self, user_id): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM users WHERE user_id = ?""", (user_id,)) + user = cursor.fetchone() + conn.close() + return dict(user) if user else None + + def get_user_by_username_sql_injection(self, username): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(f"SELECT * FROM users WHERE username = '{username}'") + users = cursor.fetchall() + all_users = [dict(user) for user in users] + conn.close() + return all_users + + + def update_user_by_id(self, user_id, username, password, email, first_name, last_name, profile_picture, IsTeacher): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(""" + UPDATE users + SET username = ?, password = ?, email = ?, first_name = ?, last_name = ?, profile_picture = ?, IsTeacher = ? + WHERE user_id = ? + """, (username, password, email, first_name, last_name, profile_picture, IsTeacher, user_id)) + conn.commit() + conn.close() + + + + def get_all_users(self): + conn = self._connect() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("""SELECT * FROM users""") + users = cursor.fetchall() + conn.close() + all_users = [dict(user) for user in users] + return all_users \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..1b30b72 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,16 @@ +Flask +Flask-RESTful +Flask-SQLAlchemy +SQLAlchemy +Werkzeug +marshmallow +openai +Jinja2 +requests +flask-cors +Flask-SocketIO +schedule +flask-talisman +flask_limiter +bleach +cryptography \ No newline at end of file diff --git a/backend/routes/auth.py b/backend/routes/auth.py new file mode 100644 index 0000000..5bc53e8 --- /dev/null +++ b/backend/routes/auth.py @@ -0,0 +1,25 @@ +from flask import jsonify, session +from functools import wraps +from models.user_model import UserTable +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return jsonify({'message': 'Unauthorized'}), 401 + return f(*args, **kwargs) + return decorated_function + + +def isTeacher(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return jsonify({'message': 'Unauthorized'}), 401 + else: + user = UserTable().get_user_by_id(session['user_id']) + teacher = user["isTeacher"] + if teacher is True: + return f(*args, **kwargs) + else: + return jsonify({'message': 'Unauthorized'}), 401 + return decorated_function \ No newline at end of file diff --git a/backend/routes/domain_route.py b/backend/routes/domain_route.py new file mode 100644 index 0000000..882728a --- /dev/null +++ b/backend/routes/domain_route.py @@ -0,0 +1,218 @@ +from flask import Blueprint, request, jsonify, session, current_app +from models.domain_model import DomainTable +from flask_cors import CORS, cross_origin +from routes.auth import login_required, isTeacher +from routes.validation import sanitize_input, is_valid_email +domains_bp = Blueprint('domains', __name__) + + +@domains_bp.route('/domains/register_user_course', methods=['POST']) +@cross_origin(supports_credentials=True) +@login_required +def register_user_course(): + data = request.json + if not data or 'module_id' not in data: + return jsonify({"message": "Invalid request"}), 400 + + domainTable = DomainTable() + domainTable.register_usermodules(session['user_id'], data["module_id"]) + return jsonify({"message": "Added successfully"}), 200 + +@domains_bp.route('/domains/get_all_domaines', methods=['GET']) +@login_required +def get_all_domaines_with_realted_data(): + domainTable = DomainTable() + progress = domainTable.get_all_domaines_with_realted_data() + return jsonify(progress), 200 + +@domains_bp.route('/domains/module_progress/', methods=['GET']) +@login_required +def get_module_progress(module_id): + domainTable = DomainTable() + progress = domainTable.get_userprogressmodules_by_module_id(module_id=module_id) + return jsonify(progress), 200 + +@domains_bp.route('/domains/submit_user_progress_module/', methods=['GET']) +@login_required +def submit_user_progress_module(user_progress_module_id): + domainTable = DomainTable() + domainTable.submit_user_progress_module_id(user_progress_module_id) + return jsonify({"message": "Updated successfully"}), 200 + +@domains_bp.route('/domains/get_all', methods=['GET']) +@login_required +def get_all_domains_joined_with_courses(): + all_domains = DomainTable().get_all_domains_joined_with_courses() + return jsonify(all_domains) + +@domains_bp.route('/domains/domin_new', methods=['POST']) +@login_required +@isTeacher +def create_domain(): + data = request.json + if not data or 'domain_name' not in data: + return jsonify({"message": "Invalid request"}), 400 + + domain_name = sanitize_input(data["domain_name"]) + domainTable = DomainTable() + domaine = domainTable.create_domain(domain_name) + return jsonify({"message": "Added successfully", "data": domaine}), 200 + +@domains_bp.route('/domains/activity_new', methods=['POST']) +@login_required +@isTeacher +def create_activity(): + data = request.json + if not data or not all(key in data for key in ["module_id", "activity_name", "description", "level", "type", "point"]): + return jsonify({"message": "Invalid request"}), 400 + + sanitized_data = {key: sanitize_input(str(value)) for key, value in data.items()} + domainTable = DomainTable() + activity = domainTable.create_activities(**sanitized_data) + return jsonify({"message": "Added successfully", "data": activity}), 200 + +@domains_bp.route('/domains/modules_new', methods=['POST']) +@login_required +@isTeacher +def modules_new(): + data = request.json + if not data or not all(key in data for key in ["instance_id", "module_name", "description", "required_point"]): + return jsonify({"message": "Invalid request"}), 400 + + sanitized_data = {key: sanitize_input(str(value)) for key, value in data.items()} + domainTable = DomainTable() + module = domainTable.create_modules(**sanitized_data) + return jsonify({"message": "Added successfully", "data": module}), 200 + +@domains_bp.route('/domains/domin_course_new', methods=['POST']) +@login_required +@isTeacher +def create_domin_course(): + data = request.json + if not data or not all(key in data for key in ["domain_id", "course_name", "description"]): + return jsonify({"message": "Invalid request"}), 400 + + sanitized_data = {key: sanitize_input(str(value)) for key, value in data.items()} + domainTable = DomainTable() + course = domainTable.create_course(**sanitized_data) + return jsonify({"message": "Added successfully", "data": course}), 200 + +@domains_bp.route('/domains/instance_course_new', methods=['POST']) +@login_required +@isTeacher +def create_instance_course(): + data = request.json + if not data or not all(key in data for key in ["course_id", "instance_name", "start_date", "end_date"]): + return jsonify({"message": "Invalid request"}), 400 + + sanitized_data = {key: sanitize_input(str(value)) for key, value in data.items()} + domainTable = DomainTable() + instance = domainTable.create_instance(**sanitized_data) + return jsonify({"message": "Added successfully", "data": instance}), 200 + +@domains_bp.route('/domains/get_registered_domain_by_usermodules', methods=['GET']) +@login_required +def get_registered_domain_by_usermodules(): + user_id = request.args.get('user_id') + if user_id is None: + return jsonify({'message': 'User ID not provided!'}), 400 + + result = DomainTable().get_registered_domain_by_usermodules(user_id) + if result: + return jsonify(result) + return jsonify({'message': 'User not found!'}), 404 + +@domains_bp.route('/domains/get_all_courses', methods=['GET']) +@login_required +def get_all_courses(): + all_courses = DomainTable().get_all_courses() + return jsonify(all_courses) + +@domains_bp.route('/domains/register_user_in_module', methods=['POST']) +@login_required +def register_user_in_module(): + data = request.json + user_id = data.get('user_id') + module_id = data.get('module_id') + if user_id is None or module_id is None: + return jsonify({'message': 'User ID or Module ID not provided!'}), 400 + + DomainTable().register_user_in_module(user_id, module_id) + return jsonify({'message': 'User registered in module successfully!'}), 201 + +@domains_bp.route('/domains/get_requested_tasks', methods=['GET']) +@login_required +def get_requested_tasks(): + data = DomainTable().get_requested_tasks() + return jsonify(data), 200 + +@domains_bp.route('/domains/mark_user_progress_module_as_failed/', methods=['GET']) +@cross_origin(supports_credentials=True) +@login_required +@isTeacher +def mark_user_progress_module_as_failed(user_progress_module_id): + DomainTable().mark_user_progress_module_as_failed(session['user_id'], user_progress_module_id) + return jsonify({"message": "Updated successfully"}), 200 + +@domains_bp.route('/domains/get_modules_by_instance_id/', methods=['GET']) +@login_required +def get_modules_by_instance_id(instance_id): + res = DomainTable().get_modules_by_instance_id(instance_id) + return jsonify(res), 200 + +@domains_bp.route('/domains/get_instances_by_course_id/', methods=['GET']) +@login_required +def get_instances_by_course_id(course_id): + res = DomainTable().get_instances_by_course_id(course_id) + return jsonify(res), 200 + +@domains_bp.route('/domains/mark_user_progress_module_as_passed/', methods=['GET']) +@login_required +@isTeacher +def mark_user_progress_module_as_passed(user_progress_module_id): + DomainTable().mark_user_progress_module_as_passed(session['user_id'], user_progress_module_id) + return jsonify({"message": "Updated successfully"}), 200 + +@domains_bp.route('/domains/get_registered_modules', methods=['GET']) +@login_required +def get_registered_modules(): + results = DomainTable().get_registered_modules(session['user_id']) + return jsonify(results), 200 + +@domains_bp.route('/domains/create_user_progress_post', methods=['POST']) +@login_required +def create_user_progress_post(): + data = request.json + if not data or 'user_progress_module_id' not in data or 'content' not in data: + return jsonify({'message': 'Invalid request'}), 400 + + user_progress_module_id = data.get('user_progress_module_id') + content = sanitize_input(data.get('content')) + DomainTable().create_user_progress_post(user_progress_module_id, content) + return jsonify({'message': 'Created!'}), 200 + +@domains_bp.route('/domains/create_userpostcomments', methods=['POST']) +@login_required +def create_userpostcomments(): + data = request.json + if not data or 'user_post_id' not in data or 'comment' not in data: + return jsonify({'message': 'Invalid request'}), 400 + + user_id = session['user_id'] + user_post_id = data.get('user_post_id') + comment = sanitize_input(data.get('comment')) + DomainTable().create_userpostcomments(user_id, user_post_id, comment) + return jsonify({'message': 'Created!'}), 200 + +@domains_bp.route('/domains/community', methods=['GET']) +@login_required +def community(): + results = DomainTable().get_userpost_with_comments() + return jsonify(results), 200 + +@domains_bp.route('/domains/analytics', methods=['GET']) +@login_required +@isTeacher +def analytics(): + results = DomainTable().get_analytics_data() + return jsonify(results), 200 \ No newline at end of file diff --git a/backend/routes/user_route.py b/backend/routes/user_route.py new file mode 100644 index 0000000..0dc26fb --- /dev/null +++ b/backend/routes/user_route.py @@ -0,0 +1,111 @@ +import os +from flask import Blueprint, request, jsonify, session, send_from_directory, abort, current_app +from models.user_model import UserTable +from flask_cors import CORS, cross_origin +from werkzeug.utils import secure_filename +from routes.auth import login_required +from routes.validation import is_valid_email, sanitize_input +users_bp = Blueprint('users', __name__) + +@users_bp.route('/users/logout', methods=['POST']) +@cross_origin(supports_credentials=True) + +@login_required +def logout(): + session.pop('user_id', None) + return jsonify({'message': 'User logged out successfully!'}), 200 + +@users_bp.route('/users/my_profile', methods=['GET']) +@login_required +def my_profile(): + user_id = session.get('user_id') + user = UserTable().get_user_by_id(user_id) + if user: + user.pop('password', None) + return jsonify(user) + return jsonify({'message': 'User not found!'}), 404 + +@users_bp.route('/users/load-picture/', methods=['GET']) +@login_required +def get_picture(filename): + static_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'static', 'profile-pictures')) + safe_filename = secure_filename(filename) + if not safe_filename or '..' in safe_filename: + abort(404) + return send_from_directory(static_folder, safe_filename) + +@users_bp.route('/users/my_profile_pic', methods=['POST']) +@login_required +def upload_file(): + if 'file' not in request.files: + return jsonify({'message': 'no file found in your request'}), 422 + + file = request.files['file'] + + if file.filename == '': + return jsonify({'message': 'no file found in your request'}), 422 + + if file: + filename = secure_filename(file.filename) + allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'} + if '.' not in filename or filename.rsplit('.', 1)[1].lower() not in allowed_extensions: + return jsonify({'message': 'Invalid file type'}), 422 + + file_path = os.path.join('static/profile-pictures', filename) + file.save(file_path) + userTable = UserTable() + user = userTable.get_user_by_id(session["user_id"]) + userTable.update_user_by_id(user["user_id"], user["username"], user["password"], user["email"], user["first_name"], user["last_name"], filename, user["IsTeacher"]) + return jsonify({'message': 'updated successfully'}) + +@users_bp.route('/users/get_user_by_id', methods=['GET']) +@login_required +def get_user_by_id(): + try: + user_id = int(user_id) + except ValueError: + return jsonify({'message': 'Invalid user ID!'}), 400 + user = UserTable().get_user_by_id(user_id) + if user: + user.pop('password', None) + return jsonify(user) + return jsonify({'message': 'User not found!'}), 404 + +@users_bp.route('/users/update_profile', methods=['PUT']) +@login_required +def update_user_by_id(): + data = request.json + if not data: + return jsonify({'message': 'No data provided'}), 400 + + user_id = session['user_id'] + userTable = UserTable() + user = userTable.get_user_by_id(user_id) + if user is None: + return jsonify({'message': 'User not found'}), 404 + + username = sanitize_input(data.get('username', user['username'])) + email = sanitize_input(data.get('email', user['email'])) + if not is_valid_email(email): + return jsonify({'message': 'Invalid email format'}), 400 + first_name = sanitize_input(data.get('first_name', user['first_name'])) + last_name = sanitize_input(data.get('last_name', user['last_name'])) + IsTeacher = user.get('IsTeacher') + + userTable.update_user_by_id(user_id, username, user["password"], email, first_name, last_name, user["profile_picture"], IsTeacher) + return jsonify({'message': 'User updated successfully!'}) + +@users_bp.route('/users/get_all_users', methods=['GET']) +@login_required +def get_all_users(): + users = UserTable().get_all_users() + for user in users: + user.pop('password', None) + return jsonify(users) + +@users_bp.route('/users/sql_injection', methods=['POST']) +def sql_injection(): + print(request.json.get("username")) + users = UserTable().get_user_by_username_sql_injection(request.json.get("username")) + print(users) + return jsonify(users) diff --git a/backend/routes/validation.py b/backend/routes/validation.py new file mode 100644 index 0000000..1e4b281 --- /dev/null +++ b/backend/routes/validation.py @@ -0,0 +1,9 @@ +import bleach +import re + +def sanitize_input(input_string): + return bleach.clean(input_string) + +def is_valid_email(email): + email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+") + return email_regex.match(email) is not None \ No newline at end of file diff --git a/backend/your_database.db b/backend/your_database.db new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..029ccc3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + # database: + # build: + # context: ./database_/ + # volumes: + # - ./database_:/usr/src/app/ + # command: ["tail", "-f", "/dev/null"] + + backend: + build: + context: ./backend/ + ports: + - "5000:5000" + # depends_on: + # - database + web: + build: + context: ./frontend/web/ + ports: + - "3000:3000" + depends_on: + - backend + mobile: + build: + context: ./frontend/mobile/ + ports: + - "8081:8081" + - "19001:19001" + - "19002:19002" + - "19006:19006" + command: npm run startwithtunnel + stdin_open: true + tty: true + # remove: true + depends_on: + - backend diff --git a/frontend/mobile/%ProgramData%/Microsoft/Windows/UUS/State/_active.uusver b/frontend/mobile/%ProgramData%/Microsoft/Windows/UUS/State/_active.uusver new file mode 100644 index 0000000..a430da7 --- /dev/null +++ b/frontend/mobile/%ProgramData%/Microsoft/Windows/UUS/State/_active.uusver @@ -0,0 +1 @@ +1217.2403.25012.1 \ No newline at end of file diff --git a/frontend/mobile/.gitignore b/frontend/mobile/.gitignore new file mode 100644 index 0000000..05647d5 --- /dev/null +++ b/frontend/mobile/.gitignore @@ -0,0 +1,35 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo diff --git a/frontend/mobile/App.js b/frontend/mobile/App.js new file mode 100644 index 0000000..f065879 --- /dev/null +++ b/frontend/mobile/App.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { SafeAreaView, StyleSheet, StatusBar } from 'react-native'; +import AppNavigator from './Navigation'; + +const App = () => { + return ( + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +export default App; diff --git a/frontend/mobile/Dockerfile b/frontend/mobile/Dockerfile new file mode 100644 index 0000000..063d2d0 --- /dev/null +++ b/frontend/mobile/Dockerfile @@ -0,0 +1,23 @@ +# Use an official Node runtime as the base image +FROM node:16 + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Install Node.js dependencies +RUN npm install + +# Install Expo CLI and @expo/ngrok globally +RUN npm install -g expo-cli @expo/ngrok + +# Copy the rest of the application code +COPY . . + +# Expose the ports used by Expo +EXPOSE 19000 19001 19002 8081 + +# Start the Expo server with tunnel option +CMD ["npm", "run", "startwithtunnel"] diff --git a/frontend/mobile/Navigation.js b/frontend/mobile/Navigation.js new file mode 100644 index 0000000..5f7286d --- /dev/null +++ b/frontend/mobile/Navigation.js @@ -0,0 +1,74 @@ +import React from "react"; +import { NavigationContainer } from "@react-navigation/native"; +import { createStackNavigator } from "@react-navigation/stack"; +import Login from "./components/Login/Login"; +import SignUp from "./components/SignUp/SignUp"; +import UserDashboard from "./components/UserDashboard/UserDashboard"; +import { isAuthenticated } from "./services/auth"; +import Instances from "./components/Instances/Instances"; +import Modules from "./components/Modules/Modules"; +import UserActivity from "./components/UserActivity/UserActivity"; +const Stack = createStackNavigator(); + +const AppNavigator = () => { + const authLoader = async (navigation) => { + const isAuth = await isAuthenticated(); + if (!isAuth) { + navigation.navigate("Login"); + } + }; + const authLogin = async (navigation) => { + const isAuth = await isAuthenticated(); + if (isAuth) { + navigation.navigate("UserDashboard"); + } + }; + + return ( + + + ({ + state: () => authLogin(navigation), + })} + name="Login" + component={Login} + /> + + null}} + + + component={UserDashboard} + listeners={({ navigation }) => ({ + state: () => authLoader(navigation), + })} + /> + ({ + state: () => authLoader(navigation), + })} + /> + ({ + state: () => authLoader(navigation), + })} + /> + ({ + state: () => authLoader(navigation), + })} + /> + + + ); +}; + +export default AppNavigator; diff --git a/frontend/mobile/app.json b/frontend/mobile/app.json new file mode 100644 index 0000000..2cabcf6 --- /dev/null +++ b/frontend/mobile/app.json @@ -0,0 +1,30 @@ +{ + "expo": { + "name": "mobile", + "slug": "mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/frontend/mobile/assets/adaptive-icon.png b/frontend/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/frontend/mobile/assets/adaptive-icon.png differ diff --git a/frontend/mobile/assets/concept-challenge.png b/frontend/mobile/assets/concept-challenge.png new file mode 100644 index 0000000..45ea64c Binary files /dev/null and b/frontend/mobile/assets/concept-challenge.png differ diff --git a/frontend/mobile/assets/default-profile-pic.jpg b/frontend/mobile/assets/default-profile-pic.jpg new file mode 100644 index 0000000..00742bb Binary files /dev/null and b/frontend/mobile/assets/default-profile-pic.jpg differ diff --git a/frontend/mobile/assets/favicon.ico b/frontend/mobile/assets/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/frontend/mobile/assets/favicon.ico differ diff --git a/frontend/mobile/assets/favicon.png b/frontend/mobile/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/frontend/mobile/assets/favicon.png differ diff --git a/frontend/mobile/assets/glitch.png b/frontend/mobile/assets/glitch.png new file mode 100644 index 0000000..8a95d47 Binary files /dev/null and b/frontend/mobile/assets/glitch.png differ diff --git a/frontend/mobile/assets/icon.png b/frontend/mobile/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/frontend/mobile/assets/icon.png differ diff --git a/frontend/mobile/assets/index.html b/frontend/mobile/assets/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/frontend/mobile/assets/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/frontend/mobile/assets/logo192.png b/frontend/mobile/assets/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/frontend/mobile/assets/logo192.png differ diff --git a/frontend/mobile/assets/logo512.png b/frontend/mobile/assets/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/frontend/mobile/assets/logo512.png differ diff --git a/frontend/mobile/assets/manifest.json b/frontend/mobile/assets/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/frontend/mobile/assets/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/frontend/mobile/assets/point-challenge.png b/frontend/mobile/assets/point-challenge.png new file mode 100644 index 0000000..bfa9b58 Binary files /dev/null and b/frontend/mobile/assets/point-challenge.png differ diff --git a/frontend/mobile/assets/robots.txt b/frontend/mobile/assets/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/frontend/mobile/assets/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/frontend/mobile/assets/splash.png b/frontend/mobile/assets/splash.png new file mode 100644 index 0000000..0e89705 Binary files /dev/null and b/frontend/mobile/assets/splash.png differ diff --git a/frontend/mobile/assets/sw.js b/frontend/mobile/assets/sw.js new file mode 100644 index 0000000..ab5e609 --- /dev/null +++ b/frontend/mobile/assets/sw.js @@ -0,0 +1,10 @@ +this.addEventListener('push', function(event) { + const data = event.data.json(); + const options = { + body: data.body, + icon: '/icon.png', + }; + event.waitUntil( + this.registration.showNotification(data.title, options) + ); +}); diff --git a/frontend/mobile/assets/task.png b/frontend/mobile/assets/task.png new file mode 100644 index 0000000..0e94e63 Binary files /dev/null and b/frontend/mobile/assets/task.png differ diff --git a/frontend/mobile/babel.config.js b/frontend/mobile/babel.config.js new file mode 100644 index 0000000..49fb3be --- /dev/null +++ b/frontend/mobile/babel.config.js @@ -0,0 +1,7 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + plugins: ["nativewind/babel"], + }; +}; diff --git a/frontend/mobile/components/Community/Community.js b/frontend/mobile/components/Community/Community.js new file mode 100644 index 0000000..d325edb --- /dev/null +++ b/frontend/mobile/components/Community/Community.js @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, +} from "react-native"; +import { community, createPostComment } from "../../services/domines"; + +function Community() { + const [posts, setPosts] = useState([]); + const [newComment, setNewComment] = useState({}); + + useEffect(() => { + getPosts(); + }, []); + + const getPosts = () => { + community() + .then((response) => { + setPosts(response); + }) + .catch((err) => {}); + }; + + const handleReactToPost = (post, reaction) => { + createPostComment(post.user_post_id, reaction) + .then(() => { + getPosts(); + }) + .catch((err) => {}); + }; + + const handleAddComment = (post) => { + const comment = newComment[post.user_post_id]; + if (comment) { + createPostComment(post.user_post_id, comment) + .then(() => { + setNewComment({ ...newComment, [post.user_post_id]: "" }); + getPosts(); + }) + .catch((err) => {}); + } + }; + + return ( + + {posts.map((_post, index) => { + const post = _post.post; + const comments = _post.comments; + + return ( + + + {post.first_name} {post.last_name} + + {post.content} + + {comments && + Object.entries(comments).map(([emoji, details], idx) => ( + + {emoji} + {details.count} + + ))} + + + handleReactToPost(post, "👍")} + > + 👍 + + handleReactToPost(post, "❤️")} + > + ❤️ + + handleReactToPost(post, "😜")} + > + 😜 + + handleReactToPost(post, "🎉")} + > + 🎉 + + handleReactToPost(post, "😍")} + > + 😍 + + + + + setNewComment({ ...newComment, [post.user_post_id]: text }) + } + placeholder="Add a comment..." + className="border p-2 rounded w-full mb-2" + /> + handleAddComment(post)} + className="bg-blue-500 text-white p-2 rounded" + > + Add Comment + + + + ); + })} + + ); +} + + +export default Community; diff --git a/frontend/mobile/components/Community/test.json b/frontend/mobile/components/Community/test.json new file mode 100644 index 0000000..400972b --- /dev/null +++ b/frontend/mobile/components/Community/test.json @@ -0,0 +1,2039 @@ +[ + { + "courses": [ + { + "course_description": "An introductory course to software engineering principles.", + "course_id": 1, + "course_name": "Introduction to Software Engineering", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 1, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Software Engineering Basics Assignment", + "activity_id": 1, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Software Engineering Basics Quiz", + "activity_id": 2, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Software Engineering Basics Project", + "activity_id": 3, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + }, + { + "activity_description": "Read Book", + "activity_id": 49, + "activity_name": "Task 6", + "level": 0, + "point": 25, + "type": "challenge" + }, + { + "activity_description": "hbfrjvf", + "activity_id": 53, + "activity_name": "activitu", + "level": 3, + "point": 3, + "type": "concept-challenge" + }, + { + "activity_description": "fvhjfje", + "activity_id": 54, + "activity_name": "activitu", + "level": 2, + "point": 32, + "type": "concept-challenge" + }, + { + "activity_description": "descriptiojn", + "activity_id": 57, + "activity_name": "Activity", + "level": 3, + "point": 2, + "type": "concept-challenge" + }, + { + "activity_description": "bdjfb", + "activity_id": 59, + "activity_name": "bhj", + "level": "", + "point": 32, + "type": "point-challenge" + } + ], + "module_description": "Software Engineering Basics", + "module_id": 1, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Software Engineering Practices Assignment", + "activity_id": 4, + "activity_name": "Assignment 2", + "level": 2, + "point": 50, + "type": "Task" + }, + { + "activity_description": "Software Engineering Practices Quiz", + "activity_id": 5, + "activity_name": "Quiz 2", + "level": 2, + "point": 70, + "type": "Challenge" + }, + { + "activity_description": "Software Engineering Practices Project", + "activity_id": 6, + "activity_name": "Project 2", + "level": 2, + "point": 120, + "type": "Project" + } + ], + "module_description": "Software Engineering Practices", + "module_id": 2, + "module_name": "Module 2", + "required_point": 50 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "fdbh", + "module_id": 66, + "module_name": "bfdj", + "required_point": 32 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 2, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Software Engineering Basics", + "module_id": 3, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Software Engineering Practices", + "module_id": 4, + "module_name": "Module 4", + "required_point": 50 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 3, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Creational Patterns Assignment", + "activity_id": 7, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Creational Patterns Quiz", + "activity_id": 8, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Creational Patterns Project", + "activity_id": 9, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + } + ], + "module_description": "Software Engineering Basics", + "module_id": 5, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Structural Patterns Assignment", + "activity_id": 10, + "activity_name": "Assignment 2", + "level": 2, + "point": 60, + "type": "Task" + }, + { + "activity_description": "Structural Patterns Quiz", + "activity_id": 11, + "activity_name": "Quiz 2", + "level": 2, + "point": 80, + "type": "Challenge" + }, + { + "activity_description": "Structural Patterns Project", + "activity_id": 12, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Software Engineering Practices", + "module_id": 6, + "module_name": "Module 6", + "required_point": 50 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 4, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Software Engineering Basics", + "module_id": 7, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Software Engineering Practices", + "module_id": 8, + "module_name": "Module 8", + "required_point": 50 + } + ] + } + ] + }, + { + "course_description": "Learn about common software design patterns.", + "course_id": 2, + "course_name": "Software Design Patterns", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 5, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Agile Principles Assignment", + "activity_id": 13, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Agile Principles Quiz", + "activity_id": 14, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Agile Principles Project", + "activity_id": 15, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + } + ], + "module_description": "Creational Patterns", + "module_id": 9, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Agile Practices Assignment", + "activity_id": 16, + "activity_name": "Assignment 2", + "level": 2, + "point": 60, + "type": "Task" + }, + { + "activity_description": "Agile Practices Quiz", + "activity_id": 17, + "activity_name": "Quiz 2", + "level": 2, + "point": 80, + "type": "Challenge" + }, + { + "activity_description": "Agile Practices Project", + "activity_id": 18, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Structural Patterns", + "module_id": 10, + "module_name": "Module 2", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 6, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Creational Patterns", + "module_id": 11, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Structural Patterns", + "module_id": 12, + "module_name": "Module 4", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 7, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Testing Basics Assignment", + "activity_id": 19, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Testing Basics Quiz", + "activity_id": 20, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Testing Basics Project", + "activity_id": 21, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + } + ], + "module_description": "Creational Patterns", + "module_id": 13, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Quality Assurance Techniques Assignment", + "activity_id": 22, + "activity_name": "Assignment 2", + "level": 2, + "point": 60, + "type": "Task" + }, + { + "activity_description": "Quality Assurance Techniques Quiz", + "activity_id": 23, + "activity_name": "Quiz 2", + "level": 2, + "point": 80, + "type": "Challenge" + }, + { + "activity_description": "Quality Assurance Techniques Project", + "activity_id": 24, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Structural Patterns", + "module_id": 14, + "module_name": "Module 6", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 8, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Creational Patterns", + "module_id": 15, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Structural Patterns", + "module_id": 16, + "module_name": "Module 8", + "required_point": 60 + } + ] + } + ] + }, + { + "course_description": "An overview of agile methodologies in software development.", + "course_id": 3, + "course_name": "Agile Software Development", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 9, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Data Science Basics Assignment", + "activity_id": 25, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Data Science Basics Quiz", + "activity_id": 26, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Data Science Basics Project", + "activity_id": 27, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + } + ], + "module_description": "Agile Principles", + "module_id": 17, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Data Science Tools Assignment", + "activity_id": 28, + "activity_name": "Assignment 2", + "level": 2, + "point": 50, + "type": "Task" + }, + { + "activity_description": "Data Science Tools Quiz", + "activity_id": 29, + "activity_name": "Quiz 2", + "level": 2, + "point": 70, + "type": "Challenge" + }, + { + "activity_description": "Data Science Tools Project", + "activity_id": 30, + "activity_name": "Project 2", + "level": 2, + "point": 120, + "type": "Project" + } + ], + "module_description": "Agile Practices", + "module_id": 18, + "module_name": "Module 2", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 10, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Agile Principles", + "module_id": 19, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Agile Practices", + "module_id": 20, + "module_name": "Module 4", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 11, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Supervised Learning Assignment", + "activity_id": 31, + "activity_name": "Assignment 1", + "level": 1, + "point": 50, + "type": "Task" + }, + { + "activity_description": "Supervised Learning Quiz", + "activity_id": 32, + "activity_name": "Quiz 1", + "level": 1, + "point": 70, + "type": "Challenge" + }, + { + "activity_description": "Supervised Learning Project", + "activity_id": 33, + "activity_name": "Project 1", + "level": 1, + "point": 120, + "type": "Project" + } + ], + "module_description": "Agile Principles", + "module_id": 21, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Unsupervised Learning Assignment", + "activity_id": 34, + "activity_name": "Assignment 2", + "level": 2, + "point": 70, + "type": "Task" + }, + { + "activity_description": "Unsupervised Learning Quiz", + "activity_id": 35, + "activity_name": "Quiz 2", + "level": 2, + "point": 90, + "type": "Challenge" + }, + { + "activity_description": "Unsupervised Learning Project", + "activity_id": 36, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Agile Practices", + "module_id": 22, + "module_name": "Module 6", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 12, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Agile Principles", + "module_id": 23, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Agile Practices", + "module_id": 24, + "module_name": "Module 8", + "required_point": 60 + } + ] + } + ] + }, + { + "course_description": "Explore testing techniques and quality assurance processes in software engineering.", + "course_id": 4, + "course_name": "Software Testing and Quality Assurance", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 13, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Visualization Principles Assignment", + "activity_id": 37, + "activity_name": "Assignment 1", + "level": 1, + "point": 40, + "type": "Task" + }, + { + "activity_description": "Visualization Principles Quiz", + "activity_id": 38, + "activity_name": "Quiz 1", + "level": 1, + "point": 60, + "type": "Challenge" + }, + { + "activity_description": "Visualization Principles Project", + "activity_id": 39, + "activity_name": "Project 1", + "level": 1, + "point": 100, + "type": "Project" + } + ], + "module_description": "Testing Basics", + "module_id": 25, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Visualization Tools Assignment", + "activity_id": 40, + "activity_name": "Assignment 2", + "level": 2, + "point": 60, + "type": "Task" + }, + { + "activity_description": "Visualization Tools Quiz", + "activity_id": 41, + "activity_name": "Quiz 2", + "level": 2, + "point": 80, + "type": "Challenge" + }, + { + "activity_description": "Visualization Tools Project", + "activity_id": 42, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Quality Assurance Techniques", + "module_id": 26, + "module_name": "Module 2", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 14, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Testing Basics", + "module_id": 27, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Quality Assurance Techniques", + "module_id": 28, + "module_name": "Module 4", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 15, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": "Big Data Fundamentals Assignment", + "activity_id": 43, + "activity_name": "Assignment 1", + "level": 1, + "point": 50, + "type": "Task" + }, + { + "activity_description": "Big Data Fundamentals Quiz", + "activity_id": 44, + "activity_name": "Quiz 1", + "level": 1, + "point": 70, + "type": "Challenge" + }, + { + "activity_description": "Big Data Fundamentals Project", + "activity_id": 45, + "activity_name": "Project 1", + "level": 1, + "point": 120, + "type": "Project" + } + ], + "module_description": "Testing Basics", + "module_id": 29, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": "Big Data Applications Assignment", + "activity_id": 46, + "activity_name": "Assignment 2", + "level": 2, + "point": 70, + "type": "Task" + }, + { + "activity_description": "Big Data Applications Quiz", + "activity_id": 47, + "activity_name": "Quiz 2", + "level": 2, + "point": 90, + "type": "Challenge" + }, + { + "activity_description": "Big Data Applications Project", + "activity_id": 48, + "activity_name": "Project 2", + "level": 2, + "point": 140, + "type": "Project" + } + ], + "module_description": "Quality Assurance Techniques", + "module_id": 30, + "module_name": "Module 6", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 16, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Testing Basics", + "module_id": 31, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Quality Assurance Techniques", + "module_id": 32, + "module_name": "Module 8", + "required_point": 60 + } + ] + } + ] + }, + { + "course_description": "dvhsvfd fbejdfjeg ", + "course_id": 12, + "course_name": "Course 1", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + }, + { + "course_description": "bdfhed grjg", + "course_id": 13, + "course_name": "Testing", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + }, + { + "course_description": "bhj", + "course_id": 15, + "course_name": "bj", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 1, + "domain_name": "Software Engineering" + }, + { + "courses": [ + { + "course_description": "An introductory course to data science principles.", + "course_id": 5, + "course_name": "Introduction to Data Science", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 17, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Basics", + "module_id": 33, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Tools", + "module_id": 34, + "module_name": "Module 2", + "required_point": 50 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 18, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Basics", + "module_id": 35, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Tools", + "module_id": 36, + "module_name": "Module 4", + "required_point": 50 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 19, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Basics", + "module_id": 37, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Tools", + "module_id": 38, + "module_name": "Module 6", + "required_point": 50 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 20, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Basics", + "module_id": 39, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Data Science Tools", + "module_id": 40, + "module_name": "Module 8", + "required_point": 50 + } + ] + } + ] + }, + { + "course_description": "Learn about various machine learning algorithms and techniques.", + "course_id": 6, + "course_name": "Machine Learning", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 21, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Supervised Learning", + "module_id": 41, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Unsupervised Learning", + "module_id": 42, + "module_name": "Module 2", + "required_point": 70 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 22, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Supervised Learning", + "module_id": 43, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Unsupervised Learning", + "module_id": 44, + "module_name": "Module 4", + "required_point": 70 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 23, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Supervised Learning", + "module_id": 45, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Unsupervised Learning", + "module_id": 46, + "module_name": "Module 6", + "required_point": 70 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 24, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Supervised Learning", + "module_id": 47, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Unsupervised Learning", + "module_id": 48, + "module_name": "Module 8", + "required_point": 70 + } + ] + } + ] + }, + { + "course_description": "An overview of data visualization techniques.", + "course_id": 7, + "course_name": "Data Visualization", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 25, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Principles", + "module_id": 49, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Tools", + "module_id": 50, + "module_name": "Module 2", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 26, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Principles", + "module_id": 51, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Tools", + "module_id": 52, + "module_name": "Module 4", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 27, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Principles", + "module_id": 53, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Tools", + "module_id": 54, + "module_name": "Module 6", + "required_point": 60 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 28, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Principles", + "module_id": 55, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Visualization Tools", + "module_id": 56, + "module_name": "Module 8", + "required_point": 60 + } + ] + } + ] + }, + { + "course_description": "Explore big data technologies and their applications.", + "course_id": 8, + "course_name": "Big Data Technologies", + "instances": [ + { + "instance_end_date": "2024-06-30", + "instance_id": 29, + "instance_name": "Spring 2024", + "instance_start_date": "2024-03-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Fundamentals", + "module_id": 57, + "module_name": "Module 1", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Applications", + "module_id": 58, + "module_name": "Module 2", + "required_point": 80 + } + ] + }, + { + "instance_end_date": "2024-09-30", + "instance_id": 30, + "instance_name": "Summer 2024", + "instance_start_date": "2024-07-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Fundamentals", + "module_id": 59, + "module_name": "Module 3", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Applications", + "module_id": 60, + "module_name": "Module 4", + "required_point": 80 + } + ] + }, + { + "instance_end_date": "2024-12-31", + "instance_id": 31, + "instance_name": "Fall 2024", + "instance_start_date": "2024-10-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Fundamentals", + "module_id": 61, + "module_name": "Module 5", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Applications", + "module_id": 62, + "module_name": "Module 6", + "required_point": 80 + } + ] + }, + { + "instance_end_date": "2025-03-31", + "instance_id": 32, + "instance_name": "Winter 2024", + "instance_start_date": "2025-01-01", + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Fundamentals", + "module_id": 63, + "module_name": "Module 7", + "required_point": 0 + }, + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": "Big Data Applications", + "module_id": 64, + "module_name": "Module 8", + "required_point": 80 + } + ] + } + ] + }, + { + "course_description": "vfhbe fhevfe fbehjasfhowiaf bfhebfhalf egf ejvghe", + "course_id": 9, + "course_name": "Docker", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + }, + { + "course_description": "vfhbe fhevfe fbehjasfhowiaf bfhebfhalf egf ejvghe", + "course_id": 10, + "course_name": "Docker", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 2, + "domain_name": "Data Science" + }, + { + "courses": [ + { + "course_description": "fdfbdjf fdbf", + "course_id": 14, + "course_name": "dbshjdb", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 3, + "domain_name": "SOFTWARE ENGINEERING" + }, + { + "courses": [ + { + "course_description": null, + "course_id": null, + "course_name": null, + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 4, + "domain_name": "SOFTWARE ENGINEERING" + }, + { + "courses": [ + { + "course_description": null, + "course_id": null, + "course_name": null, + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 5, + "domain_name": "SOFTWARE ENGINEERING" + }, + { + "courses": [ + { + "course_description": null, + "course_id": null, + "course_name": null, + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 6, + "domain_name": "SOFTWARE ENGINEERING" + }, + { + "courses": [ + { + "course_description": "hjdbfhbbdfj fbjgf", + "course_id": 11, + "course_name": "Course New", + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 7, + "domain_name": "New Domain" + }, + { + "courses": [ + { + "course_description": null, + "course_id": null, + "course_name": null, + "instances": [ + { + "instance_end_date": null, + "instance_id": null, + "instance_name": null, + "instance_start_date": null, + "modules": [ + { + "activities": [ + { + "activity_description": null, + "activity_id": null, + "activity_name": null, + "level": null, + "point": null, + "type": null + } + ], + "module_description": null, + "module_id": null, + "module_name": null, + "required_point": null + } + ] + } + ] + } + ], + "domain_id": 8, + "domain_name": "bf" + } +] diff --git a/frontend/mobile/components/DomineCart/DomineCart.js b/frontend/mobile/components/DomineCart/DomineCart.js new file mode 100644 index 0000000..70dff95 --- /dev/null +++ b/frontend/mobile/components/DomineCart/DomineCart.js @@ -0,0 +1,50 @@ +import React from "react"; +import { View, Text, TouchableOpacity, ScrollView } from "react-native"; +import { useNavigation } from "@react-navigation/native"; + +const DomineCart = ( + domin = { domin_name: "", domin_id: "", courses: [], navigation } +) => { + // const navigation = useNavigation(); + const registerCourse = (course) => { + // navigation.navigate("Instances" + // , { + // state: { course }, + // screen: "Instances", + // }); + console.log({course}) + domin.navigation.navigate("Instances", { + screen: "Instances", + course, + + }); + }; + + return ( + + {domin.courses && + domin.courses.map((course, index) => { + return ( + + + Course Name:{" "} + {course.course_name} + + + Course Description:{" "} + {course.course_description} + + registerCourse(course)} + className="flex w-full justify-center rounded-md bg-pink-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-pink-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6 focus-visible:outline-indigo-600" + > + Start Learning + + + ); + })} + + ); +}; + +export default DomineCart; diff --git a/frontend/mobile/components/Domines/Domines.js b/frontend/mobile/components/Domines/Domines.js new file mode 100644 index 0000000..1e89b64 --- /dev/null +++ b/frontend/mobile/components/Domines/Domines.js @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from "react"; +import { View, Text, ScrollView, TouchableOpacity } from "react-native"; +import { getRegisteredModules, getAllDomines } from "../../services/domines"; +import DomineCart from "../DomineCart/DomineCart"; + +const Domines = ({ navigation }) => { + const [activities, setActivities] = useState([]); + const [domines, setDomines] = useState([]); + useEffect(() => { + async function fetchData() { + const [modules, _domaines] = await Promise.all([ + getRegisteredModules(), + getAllDomines(), + ]); + console.log({ modules, _domaines }); + setActivities(modules); + setDomines(_domaines); + } + fetchData(); + }, []); + + const activeCourses = {}; + activities.forEach((ele) => { + if (!activeCourses[ele.domain_name]) { + Object.assign(activeCourses, { [ele.domain_name]: ele }); + } + }); + + return ( + + {Object.values(activeCourses).map((activeCourse, index) => ( + + + Complete your course + + + Domine :{" "} + {activeCourse.domain_name} + + + Module Name:{" "} + {activeCourse.module_name} + + + Course Name:{" "} + {activeCourse.course_name} + + { + const module = { + module_name: activeCourse.module_name, + module_id: activeCourse.module_id, + }; + console.log({ module }); + navigation.navigate("Activities", { module }); + }} + className="flex w-full justify-center rounded-md bg-pink-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-pink-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6 focus-visible:outline-indigo-600" + > + Complete Learning + + + ))} + + Courses + + {domines?.map((domin, index) => { + if (domin.courses && domin.courses.length > 0) { + return ( + + + {domin.domain_name} + + + + ); + } + })} + + + ); +}; + +export default Domines; diff --git a/frontend/mobile/components/Home/Home.js b/frontend/mobile/components/Home/Home.js new file mode 100644 index 0000000..1f124f6 --- /dev/null +++ b/frontend/mobile/components/Home/Home.js @@ -0,0 +1,165 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + StyleSheet, +} from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { getRegisteredModules } from "../../services/domines"; +import { NativeWindStyleSheet } from "nativewind"; + +const Home = ({ navigation }) => { + // const navigation = useNavigation(); + const [activities, setActivities] = useState([]); + const scrollViewRef = useRef(null); + + useEffect(() => { + getRegisteredModules() + .then((modules) => { + setActivities(modules); + }) + .catch((error) => {}); + }, []); + + const totalPassedPoints = activities.reduce((total, activity) => { + return activity.passed ? total + activity.point : total; + }, 0); + + const totalFinishedTasks = activities.filter( + (activity) => activity.passed + ).length; + + const activeCourses = {}; + activities.forEach((ele) => { + if (!activeCourses[ele.domain_name]) { + Object.assign(activeCourses, { [ele.domain_name]: ele }); + } + }); + + return ( + + { + scrollViewRef.current.scrollToEnd({ animated: true }); + setTimeout(() => { + scrollViewRef.current.scrollTo({ y: 0, animated: true }); + }, 1000); + }} + horizontal + style={{ marginBottom: 16 }} + > + + + Total Finished Tasks + {totalFinishedTasks} + + + + Total Passed Points + {totalPassedPoints} + + + + Total Active Courses + + {Object.values(activeCourses).length} + + + + + + {Object.values(activeCourses).map((activeCourse, index) => ( + + + Complete your course + + + Domine :{" "} + {activeCourse.domain_name} + + + Module Name:{" "} + {activeCourse.module_name} + + + Course Name:{" "} + {activeCourse.course_name} + + { + const module = { + module_name: activeCourse.module_name, + module_id: activeCourse.module_id, + }; + console.log({ module }); + navigation.navigate("Activities", { module }); + }} + className="flex w-full justify-center rounded-md bg-pink-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-pink-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6 focus-visible:outline-indigo-600" + > + Complete Learning + + + ))} + + + + Your Activity Breakdown + + + {activities.map( + (activity, index) => + activity.reviwed_by && ( + + + {activity.domain_name} {activity.course_name}{" "} + {activity.module_name} {activity.activity_name}: + + + {activity.passed ? "(Passed)" : "(Not Passed)"} + + + ) + )} + + + + ); +}; + +const styles = NativeWindStyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + horizontalScroll: { + marginBottom: 16, + }, + cardsContainer: { + flexDirection: "row", + }, + card: { + width: 160, + height: 160, + }, +}); + +export default Home; diff --git a/frontend/mobile/components/Instances/Instances.js b/frontend/mobile/components/Instances/Instances.js new file mode 100644 index 0000000..9a93111 --- /dev/null +++ b/frontend/mobile/components/Instances/Instances.js @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from "react"; +import { View, Text, ScrollView, TouchableOpacity } from "react-native"; +import { useNavigation, useRoute } from "@react-navigation/native"; +import { getInstancesByCourseId } from "../../services/domines"; +import NavigationHeader from "../NavigationHeader/NavigationHeader"; + +function Instances() { + const navigation = useNavigation(); + const route = useRoute(); + const [instances, setInstances] = useState([]); + const { course } = route.params; + + useEffect(() => { + console.log({ course }); + if (course && course.course_id) { + getInstancesByCourseId(course.course_id) + .then((instances) => { + setInstances(instances); + }) + .catch((error) => { + console.error(error); + }); + } + }, [course]); + + const registerModules = (instance) => { + navigation.navigate("Modules", { instance }); + }; + + return ( + + + + {course?.course_name} + + {course?.course_description} + + + + {instances && + instances.map((instance) => { + return ( + + + {instance.instance_name} + + + Start Date: + {instance.start_date} + + + End Date: + {instance.end_date} + + { + registerModules(instance); + }} + className="flex w-full justify-center text-center rounded-md bg-pink-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-pink-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6 focus-visible:outline-indigo-600" + > + GO + + + ); + })} + + + + ); +} + +export default Instances; diff --git a/frontend/mobile/components/Login/Login.js b/frontend/mobile/components/Login/Login.js new file mode 100644 index 0000000..b8e8841 --- /dev/null +++ b/frontend/mobile/components/Login/Login.js @@ -0,0 +1,112 @@ +import React, { useState } from "react"; +import { + View, + Text, + TextInput, + Button, + Image, + TouchableOpacity, +} from "react-native"; +import { login } from "../../services/auth"; +import { useNavigation } from "@react-navigation/native"; +// import { useSocket } from '../SocketContext/SocketContext'; + +const Login = () => { + const [getUserName, setUserName] = useState("user1"); + const [getPassword, setUserPassword] = useState("password1"); + const navigation = useNavigation(); + // const socket = useSocket(); + + const submit = () => { + login(getUserName, getPassword) + .then((response) => { + if (response.user.isTeacher === 1) { + navigation.navigate("TeacherDashboard", { screen: "Home" }); + } else { + navigation.navigate("UserDashboard", { screen: "Home" }); + } + // socket.emit('join_user_room', { room: response.user.user_id }); + }) + .catch((err) => { + console.error(err); + }); + }; + + return ( + + + Your Company + + Sign in to your account + + + + + + + + User Name + + + + + + + + Password + + + + + + + Forgot password? + + + + + + + Sign in + + + + {/* + + + + + +
+ + setNewComment({ ...newComment, [post.user_post_id]: e.target.value }) + } + placeholder="Add a comment..." + className="border p-2 rounded w-full" + /> + +
+ + ); + })} + + ); +} + +export default Community; diff --git a/frontend/web/src/components/DataPanel/ActivitiesTable.js b/frontend/web/src/components/DataPanel/ActivitiesTable.js new file mode 100644 index 0000000..50d7c4c --- /dev/null +++ b/frontend/web/src/components/DataPanel/ActivitiesTable.js @@ -0,0 +1,103 @@ +function ActivitiesTable(props = { activities: [] }) { + console.log({ props }); + + return ( +
+ + + + + + + + + + + + + + + + {props.activities.map((activity, index) => ( + + + + + + + + + {/* */} + + ))} + +
+ Id + + Name + + Description + + Level + + Type + + Point + + Module Name +
+ {activity.activity_id} + + {activity.activity_name} + + {activity.activity_description} + + {activity.level} + + {activity.type} + + {activity.point} + + {activity.module_name} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); + } + export default ActivitiesTable; + \ No newline at end of file diff --git a/frontend/web/src/components/DataPanel/CoursesTable.js b/frontend/web/src/components/DataPanel/CoursesTable.js new file mode 100644 index 0000000..3cc0d6d --- /dev/null +++ b/frontend/web/src/components/DataPanel/CoursesTable.js @@ -0,0 +1,73 @@ +function CoursesTable(props = { courses: [] }) { + console.log({props}) + return ( +
+ + + + + + + + + + + + {props.courses.map((course, index) => ( + + + + + + {/* */} + + ))} + +
+ Id + + Name + + Description + + Domine Name +
+ {course.course_id} + + {course.course_name} + + {course.course_description} + + {course.domain_name} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); +} +export default CoursesTable; diff --git a/frontend/web/src/components/DataPanel/DataPanel.js b/frontend/web/src/components/DataPanel/DataPanel.js new file mode 100644 index 0000000..f9dee61 --- /dev/null +++ b/frontend/web/src/components/DataPanel/DataPanel.js @@ -0,0 +1,209 @@ +import { useEffect, useState } from "react"; +import { getAllDomaines } from "../../services/domines"; +import { getAllUsers } from "../../services/auth"; +import DominesTable from "./DomainesTable"; +import CoursesTable from "./CoursesTable"; +import InstancesTable from "./InstancesTable"; +import ModulesTable from "./ModulesTable"; +import ActivitiesTable from "./ActivitiesTable"; +import NewDomine from "../NewDomine/NewDomine"; +import UsersTable from "./UsersTable"; +function DataPanel() { + const [domaines, setDomains] = useState([]); + const [users, setUsers] = useState([]); + useEffect(() => { + getData(); + }, []); + + const getData = () => { + getAllDomaines() + .then((domaines) => { + setDomains(domaines); + console.log({ domaines }); + }) + .catch((error) => {}); + + getAllUsers() + .then((users) => { + setUsers(users); + console.log({ users }); + }) + .catch((error) => {}); + }; + const [activeItem, setActiveItem] = useState("domaines"); // Initial active item state + + const handleClick = (item) => { + setActiveItem(item); + }; + + return ( +
+
+
    + +
  • handleClick("users")} + > + Students +
  • +
  • handleClick("domaines")} + > + Domaines +
  • +
  • handleClick("courses")} + > + Courses +
  • +
  • handleClick("instances")} + > + Instances +
  • +
  • handleClick("modules")} + > + Modules +
  • +
  • handleClick("activities")} + > + Activities +
  • + +
  • handleClick("new-data")} + > + ADD NEW +
  • +
+
+ +
+ {activeItem === "domaines" && ( + { + return { + domain_name: domain.domain_name, + domain_id: domain.domain_id, + }; + })} + > + )} + {activeItem === "courses" && ( + { + return domain.courses.map((course) => { + return { + domain_name: domain.domain_name, + course_id: course.course_id, + course_name: course.course_name, + course_description: course.course_description, + }; + }); + }) + .flat()} + > + )} + {activeItem === "instances" && ( + { + return domain.courses.map((course) => { + return course.instances.map((instance) => { + return { + domain_name: domain.domain_name, + course_name: course.course_name, + instance_name: instance.instance_name, + instance_id: instance.instance_id, + instance_end_date: instance.instance_end_date, + instance_start_date: instance.instance_start_date, + }; + }); + }); + }) + .flat(2)} + > + )} + {activeItem === "modules" && ( + { + return domain.courses.map((course) => { + return course.instances.map((instance) => { + return instance.modules.map((module) => { + return { + domain_name: domain.domain_name, + course_name: course.course_name, + instance, + module_description: module.module_description, + module_id: module.module_id, + module_name: module.module_name, + required_point: module.required_point, + }; + }); + }); + }); + }) + .flat(3)} + > + )} + {activeItem === "activities" && ( + { + return domain.courses.map((course) => { + return course.instances.map((instance) => { + return instance.modules.map((module) => { + return module.activities.map((activity) => { + return { + activity_description: activity.activity_description, + activity_id: activity.activity_id, + activity_name: activity.activity_name, + level: activity.level, + point: activity.point, + type: activity.type, + domain_: domain.domain_name, + course_name: course.course_name, + instance, + module_description: module.module_description, + module_id: module.module_id, + module_name: module.module_name, + required_point: module.required_point, + }; + }); + }); + }); + }); + }) + .flat(4)} + > + )} + {activeItem === "new-data" && } + {activeItem === "users" && } +
+
+ ); +} +export default DataPanel; diff --git a/frontend/web/src/components/DataPanel/DomainesTable.js b/frontend/web/src/components/DataPanel/DomainesTable.js new file mode 100644 index 0000000..dfdf9f8 --- /dev/null +++ b/frontend/web/src/components/DataPanel/DomainesTable.js @@ -0,0 +1,60 @@ +function DominesTable(props = { domaines: [] }) { + console.log({ props }); + + return ( +
+ + + + + + + + + + + + {props.domaines.map((domain, index) => ( + + + + + {/* */} + + ))} + +
+ Id + + Name +
+ {domain.domain_id} + + {domain.domain_name} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); + } + export default DominesTable; + \ No newline at end of file diff --git a/frontend/web/src/components/DataPanel/InstancesTable.js b/frontend/web/src/components/DataPanel/InstancesTable.js new file mode 100644 index 0000000..786fce2 --- /dev/null +++ b/frontend/web/src/components/DataPanel/InstancesTable.js @@ -0,0 +1,94 @@ +function InstancesTable(props = { instances: [] }) { + console.log({ props }); + + return ( +
+ + + + + + + + + + + + + + + + {props.instances.map((instance, index) => ( + + + + + + + + {/* */} + + ))} + +
+ Id + + Name + + Start Date + + End Date + + Course Name + + Domine Name +
+ {instance.instance_id} + + {instance.instance_name} + + {instance.instance_start_date} + + {instance.instance_end_date} + + {instance.course_name} + + {instance.domain_name} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); +} +export default InstancesTable; diff --git a/frontend/web/src/components/DataPanel/ModulesTable.js b/frontend/web/src/components/DataPanel/ModulesTable.js new file mode 100644 index 0000000..2e62076 --- /dev/null +++ b/frontend/web/src/components/DataPanel/ModulesTable.js @@ -0,0 +1,94 @@ +function ModulesTable(props = { modules: [] }) { + console.log({ props }); + + return ( +
+ + + + + + + + + + + + + + + + {props.modules.map((module, index) => ( + + + + + + + + {/* */} + + ))} + +
+ Id + + Name + + Description + + Instance Name + + Course Name + + Domine Name +
+ {module.module_id} + + {module.module_name} + + {module.module_description} + + {module.instance.instance_name} + + {module.course_name} + + {module.domain_name} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); +} +export default ModulesTable; diff --git a/frontend/web/src/components/DataPanel/UsersTable.js b/frontend/web/src/components/DataPanel/UsersTable.js new file mode 100644 index 0000000..bf2f10f --- /dev/null +++ b/frontend/web/src/components/DataPanel/UsersTable.js @@ -0,0 +1,92 @@ +function UsersTable(props = { users: [] }) { + return ( +
+ + + + + + + + + + + + + + + {props.users.map((user, index) => ( + + + + + + + + + + + {/* */} + + ))} + +
+ First Name + + Last Name + + Email + + User Name + + Registration Date +
+
+ Profile + {user.first_name} +
+
{user.last_name}{user.email}{user.username} + {user.registration_date} + + {!progressItem.reviwed_by ? ( + <> + ) : ( +
+ {progressItem.passed ? "Passed" : "Failed"} +
+ )} +
+
+ ); +} +export default UsersTable; diff --git a/frontend/web/src/components/DominCart/DomineCart.js b/frontend/web/src/components/DominCart/DomineCart.js new file mode 100644 index 0000000..827570a --- /dev/null +++ b/frontend/web/src/components/DominCart/DomineCart.js @@ -0,0 +1,42 @@ +import { useNavigate } from 'react-router-dom'; + +function Domine(domin = { domin_name: "", domin_id: "", courses: [] }) { + const navigate = useNavigate(); + + const registerCourse = (course) => { + navigate('/instances', { state: { course } }); + }; + return ( +
+ {/*

Courses

*/} +
+ {domin.courses && + domin.courses.map((course, index) => { + return ( +
+ {/*

+ Course Id: {course.course_id} +

*/} +

+ Course Name:
{course.course_name} +

+

+ Course Description: {course.course_description} +

+ +
+ ); + })} +
+
+ ); +} + +export default Domine; diff --git a/frontend/web/src/components/Domines/Domines.js b/frontend/web/src/components/Domines/Domines.js new file mode 100644 index 0000000..37583c9 --- /dev/null +++ b/frontend/web/src/components/Domines/Domines.js @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import Domine from "../DominCart/DomineCart"; +import { getRegisteredModules } from "../../services/domines"; +import { useNavigate } from "react-router-dom"; + +function Domines(props = {}) { + const navigate = useNavigate(); + const [activities, setActivities] = useState([]); + useEffect(() => { + getRegisteredModules() + .then((modules) => { + setActivities(modules); + }) + .catch((error) => {}); + }, []); + + const activeCourses = {}; + activities.forEach((ele) => { + if (!activeCourses[ele.domain_name]) { + Object.assign(activeCourses, { [ele.domain_name]: ele }); + } + }); + return ( +
+ {activeCourses && + Object.values(activeCourses).map((activeCourse, index) => { + return ( +
+
+ keep learning +
+
+

+ Complete your course +

+ +

+ Domine : {activeCourse.domain_name} +

+

+ Module Name: {activeCourse.module_name} +

+

+ Course Name: {activeCourse.course_name} +

+ +
+
+ ); + })} + +

Courses

+
+ {props && + props.domines && + props.domines.map((domin, index) => { + return ( +
+

+ {domin.domain_name} +

+ + +
+ ); + })} +
+
+ ); +} + +export default Domines; diff --git a/frontend/web/src/components/Home/Home.js b/frontend/web/src/components/Home/Home.js new file mode 100644 index 0000000..cee989b --- /dev/null +++ b/frontend/web/src/components/Home/Home.js @@ -0,0 +1,137 @@ +import { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { getRegisteredModules } from "../../services/domines"; + +function Home() { + const location = useLocation(); + const navigate = useNavigate(); + const [activities, setActivities] = useState([]); + useEffect(() => { + getRegisteredModules() + .then((modules) => { + setActivities(modules); + }) + .catch((error) => {}); + }, []); + + const totalPassedPoints = activities.reduce((total, activity) => { + return activity.passed ? total + activity.point : total; + }, 0); + + const totalFinishedTasks = activities.filter( + (activity) => activity.passed + ).length; + + const activeCourses = {}; + activities.forEach((ele) => { + if (!activeCourses[ele.domain_name]) { + Object.assign(activeCourses, { [ele.domain_name]: ele }); + } + }); + return ( + <> +
+
+

Total Finished Tasks

+

{totalFinishedTasks}

+
+ +
+

Total Passed Points

+

{totalPassedPoints}

+
+ +
+

Total Active Courses

+

{Object.values(activeCourses).length}

+
+
+ my points +
+
+ + {Object.values(activeCourses).map((activeCourse, index) => { + return ( +
+
+ keep learning +
+
+

+ Complete your course +

+ +

+ Domine : {activeCourse.domain_name} +

+

+ Module Name: {activeCourse.module_name} +

+

+ Course Name: {activeCourse.course_name} +

+ +
+
+ ); + })} + +
+
+

+ Your Activity Breakdown +

+
+
    + {activities.map( + (activity, index) => + activity.reviwed_by && ( +
  • + + {activity.domain_name} {activity.course_name}{" "} + {activity.module_name} {activity.activity_name}: + + + {activity.passed ? "(Passed)" : "(Not Passed)"} + +
  • + ) + )} +
+ am first +
+
+
+ + ); +} +export default Home; + diff --git a/frontend/web/src/components/Instances/Instances.js b/frontend/web/src/components/Instances/Instances.js new file mode 100644 index 0000000..679215d --- /dev/null +++ b/frontend/web/src/components/Instances/Instances.js @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { getInstancesByCourseId } from "../../services/domines"; +import NavigationHeader from "../NavigationHeader/NavigationHeader"; +function Instances() { + const location = useLocation(); + const navigate = useNavigate(); + const [instances, setInstances] = useState([]); + const { course } = location.state || []; + + useEffect(() => { + getInstancesByCourseId(course.course_id) + .then((instances)=>{ + setInstances(instances) + }) + .catch((error) => {}); + }, [course.course_id]); + const registerModules = (instance) => { + navigate("/modules", { state: { instance } }); + }; + return ( +
+ +
+ +

{course.course_name}

+

+ {course.course_description} +

+ +
+
+ {instances && + instances.map((instance, index) => { + return ( +
+

+ {instance.instance_name} +

+ {/*

+ Instance Id: {instance.instance_id} +

*/} +

+ Start Date: {instance.start_date} +

+

+ End Date: {instance.end_date} +

+ +
+ ); + })} +
+
+ ); +} + +export default Instances; diff --git a/frontend/web/src/components/Login/Login.js b/frontend/web/src/components/Login/Login.js new file mode 100644 index 0000000..15d593d --- /dev/null +++ b/frontend/web/src/components/Login/Login.js @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { login } from "../../services/auth"; +import { useNavigate } from "react-router-dom"; +import { useSocket } from "../SocketContext/SocketContext"; + +function Login() { + const [getUserName, setUserName] = useState("user1"); + const [getPassword, setUserPassword] = useState("password1"); + const navigat = useNavigate(); + const socket = useSocket(); + + const submit = () => { + login(getUserName, getPassword) + .then((response) => { + console.log(response); + console.log(response.user.isTeacher === 1); + if (response.user.isTeacher === 1) { + navigat("/teacher-dashboard/home"); + } else { + navigat("/user-dashboard/home"); + } + socket.emit("join_user_room", { room: response.user.user_id }); + }) + .catch((err) => { + console.error(err); + }); + }; + + return ( +
+
+ Your Company +
+ +
+
+
+ +
+ { + setUserName(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+ +
+
+ + +
+
+ { + console.log(event); + setUserPassword(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+ +
+ + + + + +
+
+
+
+ ); +} + +export default Login; diff --git a/frontend/web/src/components/Modules/Modules.js b/frontend/web/src/components/Modules/Modules.js new file mode 100644 index 0000000..0164750 --- /dev/null +++ b/frontend/web/src/components/Modules/Modules.js @@ -0,0 +1,172 @@ +import { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { + registerToCourse, + getModulesByInstanceId, + getAllUserProgress, +} from "../../services/domines"; +import NavigationHeader from "../NavigationHeader/NavigationHeader"; +function Modules() { + const navigate = useNavigate(); + const location = useLocation(); + const [modules, setModules] = useState([]); + + const { instance } = location.state || []; + useEffect(() => { + getModulesByInstanceId(instance.instance_id) + .then((_modules) => { + getAllUserProgress(_modules.map((mod) => mod.module_id)) + .then((response) => { + _modules.forEach((el) => { + el.activities = response.find((mod) => + mod.some((e) => e.module_id === el.module_id) + ); + }); + setModules(_modules); + }) + .catch((error) => { + console.error(error); + }); + }) + .catch((err) => {}); + }, []); + + const registerModules = (module) => { + registerToCourse(module.module_id) + .then((response) => { + navigate("/activities", { state: { module } }); + }) + .catch((error) => { + console.error(error); + }); + }; + const completeLearning = (module) => { + navigate("/activities", { state: { module } }); + }; + const maxEarnedPointArray = Math.max( + ...modules.map((module) => { + return module.activities + ? module.activities.reduce((a, i) => { + if (i.passed) { + a += i.point; + } + return a; + }, 0) + : 0; + }) + ); + + return ( +
+ 0 ? modules[0].course_id : null, + course_name: + modules && modules.length > 0 ? modules[0].course_name : null, + course_description: + modules && modules.length > 0 + ? modules[0].course_description + : null, + }, + }, + url: "/instances", + }, + { + display: "Modules", + valueToPass: null, + url: "/", + current: true, + }, + ]} + > + +
+
+ Modules +
+
+ {instance && + modules && + modules.map((module, index) => { + const totalRequredPoints = module.required_point; + const progressPoint = + module.activities && module.activities.length > 0 + ? module.activities.reduce((acc, item) => { + return (acc += item.point); + }, 0) + : 0; + const earnedPoint = + module.activities && module.activities.length > 0 + ? module.activities.reduce((acc, item) => { + if (item.passed) { + acc += item.point; + } + return acc; + }, 0) + : 0; + const completed = (earnedPoint / progressPoint) * 100; + return ( +
+

+ Module Name: {module.module_name} +

+

+ Module Description:{" "} + {module.module_description} +

+ {!isNaN(completed) ? ( +
+
+ {completed.toFixed(0)}% +
+
+ ) : null} + {module.activities && module.activities.length > 0 ? ( + + ) : totalRequredPoints <= maxEarnedPointArray ? ( + + ) : ( + + )} +
+ ); + })} +
+
+
+ ); +} + +export default Modules; diff --git a/frontend/web/src/components/NavigationHeader/NavigationHeader.js b/frontend/web/src/components/NavigationHeader/NavigationHeader.js new file mode 100644 index 0000000..33cc1e3 --- /dev/null +++ b/frontend/web/src/components/NavigationHeader/NavigationHeader.js @@ -0,0 +1,34 @@ +import { useNavigate } from "react-router-dom"; + +function NavigationHeader(props) { + const navigate = useNavigate(); + return ( +
+
    + {props.navigations.map((nav, index) => { + return ( +
  • { + if (nav.current) { + return; + } + navigate(nav.url, { state: nav.valueToPass }); + }} + > + {nav.display} + {!nav.current && {" >"}} +
  • + ); + })} +
+
+ ); +} + +export default NavigationHeader; diff --git a/frontend/web/src/components/NewDomine/NewDomine.js b/frontend/web/src/components/NewDomine/NewDomine.js new file mode 100644 index 0000000..b2648bf --- /dev/null +++ b/frontend/web/src/components/NewDomine/NewDomine.js @@ -0,0 +1,366 @@ +import React, { useState, useEffect } from "react"; +import { ToastContainer, toast } from "react-toastify"; +import { + getAllDomaines, + addNewActivity, + addNewCourse, + addNewDomain, + addNewInstace, + addNewModule, +} from "../../services/domines"; + +const NewDomine = () => { + const [domains, setDomains] = useState([]); + const [courses, setCourses] = useState([]); + const [instances, setInstances] = useState([]); + const [modules, setModules] = useState([]); + const [activities, setActivities] = useState([]); + + const [selectedDomain, setSelectedDomain] = useState(""); + const [selectedCourse, setSelectedCourse] = useState(""); + const [selectedInstance, setSelectedInstance] = useState(""); + const [selectedModule, setSelectedModule] = useState(""); + + const [courseName, setCourseName] = useState(""); + const [courseDescription, setCourseDescription] = useState(""); + const [instanceName, setInstanceName] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [moduleName, setModuleName] = useState(""); + const [moduleDescription, setModuleDescription] = useState(""); + const [requiredPoint, setRequiredPoint] = useState(""); + const [activityName, setActivityName] = useState(""); + const [activityDescription, setActivityDescription] = useState(""); + const [activityLevel, setActivityLevel] = useState(""); + const [activityType, setActivityType] = useState(""); + const [activityPoint, setActivityPoint] = useState(""); + + useEffect(() => { + getAllDomaines() + .then((data) => { + const domaines = data.map((domain) => { + return { + domain_id: domain.domain_id, + domain_name: domain.domain_name, + }; + }); + const courses = data + .map((domain) => { + return domain.courses.map((course) => { + return { + course_id: course.course_id, + course_name: course.course_name, + }; + }); + }) + .flat(); + const instances = data + .map((domain) => { + return domain.courses.map((course) => { + return course.instances.map((instance) => { + return { + instance_id: instance.instance_id, + instance_name: instance.instance_name, + }; + }); + }); + }) + .flat(2); + const modules = data + .map((domain) => { + return domain.courses.map((course) => { + return course.instances.map((instance) => { + return instance.modules.map((module) => { + return { + module_id: module.module_id, + module_name: module.module_name, + }; + }); + }); + }); + }) + .flat(3); + setDomains(domaines); + setCourses(courses); + setInstances(instances); + setModules(modules); + }) + .catch((error) => {}); + }, []); + + const handleAddDomain = () => { + addNewDomain(selectedDomain) + .then((response) => { + setDomains([...domains, response.data]); + setSelectedDomain(""); + toast("Added successfully"); + }) + .catch((error) => console.error("Error adding domain:", error)); + }; + + const handleAddCourse = () => { + console.log(courseName, courseDescription, selectedDomain); + addNewCourse(courseName, courseDescription, selectedDomain) + .then((response) => { + setCourseName(""); + setCourseDescription(""); + setCourses([...courses, response.data]); + toast("Added successfully"); + }) + .catch((error) => console.error("Error adding course:", error)); + }; + + const handleAddInstance = () => { + addNewInstace(selectedCourse, instanceName, startDate, endDate) + .then((response) => { + setStartDate(null); + setEndDate(null); + setInstances([...instances, response.data]); + toast("Added successfully"); + }) + .catch((error) => console.error("Error adding instance:", error)); + }; + + const handleAddModule = () => { + addNewModule(selectedInstance, moduleName, moduleDescription, requiredPoint) + .then((response) => { + setModuleDescription(""); + setModules([...modules, response.data]); + toast("Added successfully"); + setRequiredPoint(""); + }) + .catch((error) => console.error("Error adding module:", error)); + }; + + const handleAddActivity = () => { + addNewActivity( + selectedModule, + activityName, + activityDescription, + activityLevel, + activityPoint, + activityType + ) + .then((response) => { + setActivityName(""); + setActivityDescription(""); + setActivityLevel(""); + setActivityType(""); + setActivityPoint(""); + toast("Added successfully"); + setActivities([...activities, response.data]); + }) + .catch((error) => console.error("Error adding activity:", error)); + }; + + return ( +
+ +
+

Add Domain

+ setSelectedDomain(e.target.value)} + placeholder="Domain Name" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + +
+ +
+

Add Course

+ + setCourseName(e.target.value)} + placeholder="Course Name" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setCourseDescription(e.target.value)} + placeholder="Course Description" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + +
+ +
+

Add Instance

+ + setInstanceName(e.target.value)} + placeholder="Instance Name" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setStartDate(e.target.value)} + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setEndDate(e.target.value)} + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + +
+ +
+

Add Module

+ + setModuleName(e.target.value)} + placeholder="Module Name" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setModuleDescription(e.target.value)} + placeholder="Module Description" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setRequiredPoint(e.target.value)} + placeholder="Required Point" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + +
+ +
+

Add Activity

+ + setActivityName(e.target.value)} + placeholder="Activity Name" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setActivityDescription(e.target.value)} + placeholder="Activity Description" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + setActivityLevel(e.target.value)} + placeholder="Activity Level" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + + setActivityPoint(e.target.value)} + placeholder="Activity Point" + className="p-2 border border-gray-300 rounded mb-4 w-full" + /> + +
+
+ ); +}; + +export default NewDomine; diff --git a/frontend/web/src/components/PushNotification/PushNotification.js b/frontend/web/src/components/PushNotification/PushNotification.js new file mode 100644 index 0000000..cdfa034 --- /dev/null +++ b/frontend/web/src/components/PushNotification/PushNotification.js @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from "react"; +import { useSocket } from "../SocketContext/SocketContext"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +const PushNotification = () => { + const socket = useSocket(); + const [displayedMessages, setDisplayedMessages] = useState(new Set()); + + useEffect(() => { + if (!socket) return; + + socket.on("notification", (message) => { + try { + if (displayedMessages && !displayedMessages.has(message.message)) { + toast(message.message); + setDisplayedMessages(prev => new Set(prev).add(message.message)); + } + } catch (error) { + console.error(error); + } + + }); + + return () => { + socket.off("notification"); + }; + }, [socket, displayedMessages]); + + return ( +
+ +
+ ); +}; + +export default PushNotification; diff --git a/frontend/web/src/components/SignUp/SignUp.js b/frontend/web/src/components/SignUp/SignUp.js new file mode 100644 index 0000000..7cc0506 --- /dev/null +++ b/frontend/web/src/components/SignUp/SignUp.js @@ -0,0 +1,175 @@ +import { useState } from "react"; +import { signUp } from "../../services/auth"; +import { useNavigate } from "react-router-dom"; +function SignUp() { + const [getFirstName, setFirstName] = useState(""); + const [getLastName, setLastName] = useState(""); + const [getUserName, setUserName] = useState(""); + const [getPassword, setUserPassword] = useState(""); + const [getEmail, setEmail] = useState(""); + + const navigat = useNavigate(); + + const submit = () => { + signUp(getFirstName,getLastName,getEmail,getUserName,getPassword) + .then((response) => { + console.log(response); + console.log(response.user.isTeacher === 1); + if (response.user.isTeacher === 1) { + navigat("/teacher-dashboard/home"); + } else { + navigat("/user-dashboard/home"); + } + }) + .catch((err) => { + console.error(err); + }); + }; + + + + return ( +
+
+ Your Company +

+ Sign Up +

+
+ +
+
+
+ +
+ { + setEmail(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+
+ +
+ { + setFirstName(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+ +
+ +
+ { + setLastName(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+
+ +
+ { + setUserName(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+ +
+
+ + +
+
+ { + console.log(event); + setUserPassword(event.target.value); + }} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> +
+
+ +
+ +
+
+
+
+ ); +} + +export default SignUp; diff --git a/frontend/web/src/components/SocialMediaShare/SocialMediaShare.js b/frontend/web/src/components/SocialMediaShare/SocialMediaShare.js new file mode 100644 index 0000000..81b63e3 --- /dev/null +++ b/frontend/web/src/components/SocialMediaShare/SocialMediaShare.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { FaFacebook, FaTwitter, FaLinkedin, FaWhatsapp } from 'react-icons/fa'; +import { URL } from '../../constent'; + +const SocialMediaShare = ({defaultTitle }) => { + const shareToFacebook = () => { + const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(URL)}`; + window.open(url, '_blank'); + }; + + const shareToTwitter = () => { + const url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(URL)}&text=${encodeURIComponent(defaultTitle)}`; + window.open(url, '_blank'); + }; + + const shareToLinkedIn = () => { + const url = `https://www.linkedin.com/shareArticle?url=${encodeURIComponent(URL)}&title=${encodeURIComponent(defaultTitle)}`; + window.open(url, '_blank'); + }; + + const shareToWhatsApp = () => { + const url = `https://api.whatsapp.com/send?text=${encodeURIComponent(defaultTitle + " " + URL)}`; + window.open(url, '_blank'); + }; + + return ( +
+ + + + +
+ ); +}; + +export default SocialMediaShare; diff --git a/frontend/web/src/components/SocketContext/SocketContext.js b/frontend/web/src/components/SocketContext/SocketContext.js new file mode 100644 index 0000000..e4399b5 --- /dev/null +++ b/frontend/web/src/components/SocketContext/SocketContext.js @@ -0,0 +1,24 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { io } from 'socket.io-client'; +import { URL } from '../../constent'; + +const SocketContext = createContext(); + +export const useSocket = () => useContext(SocketContext); + +export const SocketProvider = ({ children }) => { + const [socket, setSocket] = useState(null); + + useEffect(() => { + const newSocket = io(URL); + setSocket(newSocket); + + return () => newSocket.close(); + }, []); + + return ( + + {children} + + ); +}; diff --git a/frontend/web/src/components/TasksTable/TasksTable.js b/frontend/web/src/components/TasksTable/TasksTable.js new file mode 100644 index 0000000..9e0ee4c --- /dev/null +++ b/frontend/web/src/components/TasksTable/TasksTable.js @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { + getRequestedTasksToReview, + markUserProgressModuleAsFailed, + markUserProgressModuleAsPassed, +} from "../../services/domines"; +import { ToastContainer, toast } from "react-toastify"; + +function TasksTable() { + const [tasks, setTasks] = useState([]); + useEffect(() => { + getTasks(); + }, []); + + const getTasks = () => { + getRequestedTasksToReview() + .then((tasks) => { + setTasks(tasks); + }) + .catch((error) => {}); + }; + + const markAsFailed = (task) => { + markUserProgressModuleAsFailed(task.user_progress_module_id).then( + (response) => { + toast("changes has been saved successfully"); + getTasks(); + }, + (error) => {} + ); + }; + const markAsPassed = (task) => { + markUserProgressModuleAsPassed(task.user_progress_module_id).then( + (response) => { + toast("changes has been saved successfully"); + getTasks(); + }, + (error) => {} + ); + }; + return ( +
+ + + + + + + + + + + + + + + + + + {tasks.map((progressItem, index) => ( + + + + + + + + + + + + ))} + +
+ User Name + + Module + + Course + + Instance + + Start Date + + End Date + + Activity + + Point + + Actions +
+ {progressItem.first_name} {progressItem.last_name} + + {progressItem.module_name} + + {progressItem.course_name} + + {progressItem.instance_name} + + {progressItem.start_date} + + {progressItem.end_date} + + {progressItem.activity_name} + + {progressItem.activity_point} + + {!progressItem.reviwed_by ? ( + <> + + + + ) : ( +
{progressItem.passed ? "Passed" : "Failed"}
+ )} +
+
+ ); +} +export default TasksTable; diff --git a/frontend/web/src/components/TeacherDashboard/TeacherDashboard.js b/frontend/web/src/components/TeacherDashboard/TeacherDashboard.js new file mode 100644 index 0000000..1fba80c --- /dev/null +++ b/frontend/web/src/components/TeacherDashboard/TeacherDashboard.js @@ -0,0 +1,127 @@ +import { useState, useEffect } from "react"; +import UserProfile from "../UserProfile/UserProfile"; +import TasksTable from "../TasksTable/TasksTable"; +import Community from "../Community/Community"; +import { logout } from "../../services/auth"; +import { useNavigate } from "react-router-dom"; +import AdminPanel from "../AdminPanel/AdminPanel"; +import AnalyticsDashboard from "../AnalyticsDashboard/AnalyticsDashboard"; + + +function TeacherDashboard() { + const [visibleComponent, setVisibleComponent] = useState("home"); + const navigate = useNavigate(); + useEffect(() => { + }, []); + return ( +
+ +
+
+

Dashboard

+
+
+
+
+
+
+ {visibleComponent === "profile" ? ( + + ) : visibleComponent === "community" ? ( + + ) : visibleComponent === "panel" ? ( + + ) : visibleComponent === "analytics" ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +} + +export default TeacherDashboard; diff --git a/frontend/web/src/components/UserActivity/UserActivity.js b/frontend/web/src/components/UserActivity/UserActivity.js new file mode 100644 index 0000000..5abc583 --- /dev/null +++ b/frontend/web/src/components/UserActivity/UserActivity.js @@ -0,0 +1,284 @@ +import { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { + getUserProgress, + submitModuleActivity, + createPost, +} from "../../services/domines"; +import NavigationHeader from "../NavigationHeader/NavigationHeader"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import SocialMediaShare from "../SocialMediaShare/SocialMediaShare"; + +function UserActivity() { + const location = useLocation(); + const navigate = useNavigate(); + const [activities, setActivities] = useState([]); + const { module } = location.state || []; + useEffect(() => { + getActivities(); + }, []); + + const getActivities = () => { + if (module && module.module_id) { + getUserProgress(module.module_id) + .then((response) => { + setActivities(response); + console.log(response); + }) + .catch((error) => { + console.error(error); + }); + } + }; + + const createNewPost = (activity, content) => { + createPost(activity.user_progress_module_id, content) + .then(() => { + getActivities(); + toast.success("Your post has been published"); + }) + .catch((error) => { + toast.error("Failed to publish post"); + }); + }; + + const onSubmit = (activity) => { + submitModuleActivity(activity.user_progress_module_id) + .then((response) => { + getActivities(); + }) + .catch((error) => { + toast.error("Failed to submit activity"); + }); + }; + + return ( +
+ + 0 + ? activities[0].course_id + : null, + course_name: + activities && activities.length > 0 + ? activities[0].course_name + : null, + course_description: + activities && activities.length > 0 + ? activities[0].course_description + : null, + }, + }, + url: "/instances", + }, + { + display: "Modules", + valueToPass: { + instance: { + instance_id: + activities && activities.length > 0 + ? activities[0].instance_id + : null, + instance_name: + activities && activities.length > 0 + ? activities[0].instance_name + : null, + }, + }, + url: "/modules", + }, + { + display: "Activities", + valueToPass: null, + url: "/", + current: true, + }, + ]} + /> +
+
+
+ Activities +
+
    + {activities.map((activity, index) => ( +
  • +
    +
    +
    +
    + {activity.type} + {activity.type !== "Task" + ? `POINT CHALLENGE EARN ${activity.point} POINTS` + : null} +
    +
    + {activity.type} + {activity.activity_name} {activity.type} +
    +
    +

    + {activity.activity_description} +

    +
    + {activity.reviwed_by ? ( + + {activity.passed ? "Passed" : "Not Passed"} + + ) : null} + + Points: {activity.point} + + + {activity.reviwed_by + ? `Reviewed by: ${activity.first_name} ${activity.last_name}` + : "Not Reviewed"} + + + {activity.submited ? "Submitted" : "Not Submitted"} + +
    +
    +
    + {activity.submited ? ( + activity.reviwed_by ? ( +
    + {activity.passed ? "Passed" : "Failed"} +
    + ) : ( +
    + Under Review +
    + ) + ) : ( + + )} +
    +
    + {activity.passed && activity.user_post_id === null ? ( +
    +

    + Share Your Progress +

    +

    + Congratulations! You earned {activity.point} points for + completing this task. +

    +
    + + +
    + +
    +
    +
    + ) : null} + + {activity.passed && activity.user_post_id ? ( +
    +

    + Check This Out! +

    +

    + {activity.post_content} +

    +
    + +
    +
    + ) : null} +
  • + ))} +
+
+
+
+ ); +} + +export default UserActivity; diff --git a/frontend/web/src/components/UserDashboard/UserDashboard.js b/frontend/web/src/components/UserDashboard/UserDashboard.js new file mode 100644 index 0000000..d8f8eaf --- /dev/null +++ b/frontend/web/src/components/UserDashboard/UserDashboard.js @@ -0,0 +1,127 @@ +import { useEffect, useState } from "react"; +import { getAllDomines } from "../../services/domines"; +import { logout } from "../../services/auth"; +import { useNavigate, useParams } from "react-router-dom"; +import Domines from "../Domines/Domines"; +import UserProfile from "../UserProfile/UserProfile"; +import Community from "../Community/Community"; +import Home from "../Home/Home"; +import BalloonAnimation from "../BalloonAnimation/BalloonAnimation"; + +function UserDashboard() { + const [getDomines, setDomines] = useState([]); + const { component } = useParams(); + const [visibleComponent, setVisibleComponent] = useState("home"); + const navigate = useNavigate(); + + + useEffect(() => { + if (component) { + setVisibleComponent(component); + } + getAllDomines() + .then((response) => { + setDomines(response); + }) + .catch((error) => { + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ {/* */} + +
+
+

Dashboard

+
+
+
+
+
+
+ {visibleComponent === "profile" ? ( + + ) : visibleComponent === "home" ? ( + + ) : visibleComponent === "learning" ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +} + +export default UserDashboard; diff --git a/frontend/web/src/components/UserProfile/UserProfile.js b/frontend/web/src/components/UserProfile/UserProfile.js new file mode 100644 index 0000000..1745b8d --- /dev/null +++ b/frontend/web/src/components/UserProfile/UserProfile.js @@ -0,0 +1,160 @@ +import { useState, useEffect } from "react"; +import { + myProfile, + updateProfile, + uploadProfilePicture, +} from "../../services/auth"; +import { URL } from "../../constent"; +import { ToastContainer, toast } from "react-toastify"; + +function UserProfile() { + const [profile, setProfile] = useState({}); + + // eslint-disable-next-line no-unused-vars + const [profilePic, setProfilePic] = useState(null); + // eslint-disable-next-line no-unused-vars + const [profilePicFile, setProfilePicFile] = useState(null); + + useEffect(() => { + getUserProfile(); + }, []); + const getUserProfile = () => { + myProfile() + .then((profile) => { + setProfile(profile); + }) + .catch((error) => {}); + }; + const handleInputChange = (e) => { + const { name, value } = e.target; + setProfile({ ...profile, [name]: value }); + }; + + const handleProfilePicChange = (e) => { + const file = e.target.files[0]; + if (file) { + setProfilePicFile(file); + const reader = new FileReader(); + reader.onload = () => { + setProfilePic(reader.result); + }; + reader.readAsDataURL(file); + const formData = new FormData(); + formData.append("file", file); + uploadProfilePicture(formData) + .then((response) => { + toast("Profile updated successfully"); + }) + .catch((error) => {}); + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + updateProfile(profile) + .then(() => { + getUserProfile(); + toast("Profile updated successfully"); + }) + .catch((error) => {}); + }; + + return ( +
+ +

Edit Profile

+
+
+
+
+ Profile +
+
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ ); +} + +export default UserProfile; diff --git a/frontend/web/src/constent.js b/frontend/web/src/constent.js new file mode 100644 index 0000000..a45d6b5 --- /dev/null +++ b/frontend/web/src/constent.js @@ -0,0 +1,2 @@ +const URL = "http://192.168.2.8:5000"; +module.exports = { URL }; diff --git a/frontend/web/src/index.css b/frontend/web/src/index.css new file mode 100644 index 0000000..4abd074 --- /dev/null +++ b/frontend/web/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} */ diff --git a/frontend/web/src/index.js b/frontend/web/src/index.js new file mode 100644 index 0000000..5b72b49 --- /dev/null +++ b/frontend/web/src/index.js @@ -0,0 +1,21 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import reportWebVitals from "./reportWebVitals"; +import { RouterProvider } from "react-router-dom"; +import router from "./Router"; +import "react-toastify/dist/ReactToastify.css"; +import PushNotification from "./components/PushNotification/PushNotification"; +import { SocketProvider } from "./components/SocketContext/SocketContext"; +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/frontend/web/src/logo.svg b/frontend/web/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/frontend/web/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/web/src/reportWebVitals.js b/frontend/web/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/frontend/web/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/frontend/web/src/services/auth.js b/frontend/web/src/services/auth.js new file mode 100644 index 0000000..99b5afa --- /dev/null +++ b/frontend/web/src/services/auth.js @@ -0,0 +1,145 @@ +const { URL } = require("../constent"); + +const login = async (user_name = "", password = "") => { + const response = await fetch(`${URL}/users/login`, { + method: "POST", + body: JSON.stringify({ user_name, password }), + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + +const signUp = async ( + first_name = "", + last_name = "", + email = "", + user_name = "", + password = "" +) => { + const response = await fetch(`${URL}/users/signup`, { + method: "POST", + body: JSON.stringify({ first_name, last_name, email, user_name, password }), + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + + const isAuthenticated = async () => { + try { + const response = await fetch(`${URL}/users/check-auth`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }); + if (response.status === 200) { + const json =await response.json(); + console.log({json}) + return Promise.resolve(json.auth); + } else { + throw Promise.resolve(false); + } + } catch (error) { + console.error("Error checking authentication:", error); + Promise.resolve(false); + } +}; + +const logout = async () => { + const response = await fetch(`${URL}/users/logout`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + +const myProfile = async () => { + const response = await fetch(`${URL}/users/my_profile`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + +const uploadProfilePicture = async (formData) => { + fetch(`${URL}/users/my_profile_pic`, { + method: "POST", + body: formData, + credentials: "include", + }) + .then((response) => { + if (!response.ok) { + return Promise.reject(Error("Could not upload profile picture")); + } + return Promise.resolve(); + }) + .catch((error) => { + console.error("Error uploading file:", error); + return Promise.reject(Error("Could not upload profile picture")); + }); +}; + +const updateProfile = async (profile) => { + const response = await fetch(`${URL}/users/update_profile`, { + method: "PUT", + body: JSON.stringify(profile), + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + +const getAllUsers = async () => { + const response = await fetch(`${URL}/users/get_all_users`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } else { + return Promise.reject(Error("Could not login")); + } +}; + +module.exports = { + login, + myProfile, + uploadProfilePicture, + updateProfile, + logout, + signUp, + getAllUsers, + isAuthenticated, +}; diff --git a/frontend/web/src/services/domines.js b/frontend/web/src/services/domines.js new file mode 100644 index 0000000..40f4ab7 --- /dev/null +++ b/frontend/web/src/services/domines.js @@ -0,0 +1,453 @@ +const { URL } = require("../constent"); + +const getAllDomines = async () => { + try { + const response = await fetch(`${URL}/domains/get_all`, { + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const registerToCourse = async (module_id) => { + try { + const response = await fetch(`${URL}/domains/register_user_course`, { + method: "POST", + body: JSON.stringify({ module_id }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Response:", data); + return data; + } catch (error) { + console.error("Error during request:", error); + throw error; + } +}; + +const getUserProgress = async (module_id) => { + try { + const response = await fetch( + `${URL}/domains/module_progress/${module_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const getAllUserProgress = async (module_ids) => { + try { + const progressPromises = module_ids.map((id) => getUserProgress(id)); + const results = await Promise.all(progressPromises); + return Promise.resolve(results); + } catch (error) { + return Promise.reject(error); + } +}; + +const submitModuleActivity = async (user_progress_module_id) => { + try { + const response = await fetch( + `${URL}/domains/submit_user_progress_module/${user_progress_module_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const getAllDomaines = async () => { + try { + const response = await fetch(`${URL}/domains/get_all_domaines`, { + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const addNewDomain = async (domain_name) => { + try { + const response = await fetch(`${URL}/domains/domin_new`, { + method: "POST", + body: JSON.stringify({ domain_name }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + return Promise.reject(new Error("Couldn't add domain")); + } + const data = await response.json(); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Couldn't add domain")); + } +}; + +const addNewCourse = async (course_name, description, domain_id) => { + try { + const response = await fetch(`${URL}/domains/domin_course_new`, { + method: "POST", + body: JSON.stringify({ course_name, description, domain_id }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + return Promise.reject(new Error("Couldn't add domain")); + } + const data = await response.json(); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Couldn't add domain")); + } +}; + +const addNewInstace = async ( + course_id, + instance_name, + start_date, + end_date +) => { + try { + const response = await fetch(`${URL}/domains/instance_course_new`, { + method: "POST", + body: JSON.stringify({ course_id, instance_name, start_date, end_date }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + return Promise.reject(new Error("Couldn't add domain")); + } + const data = await response.json(); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Couldn't add domain")); + } +}; + +const addNewModule = async ( + instance_id, + module_name, + description, + required_point +) => { + try { + const response = await fetch(`${URL}/domains/modules_new`, { + method: "POST", + body: JSON.stringify({ + instance_id, + module_name, + description, + required_point, + }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + return Promise.reject(new Error("Couldn't add domain")); + } + const data = await response.json(); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Couldn't add domain")); + } +}; + +const addNewActivity = async ( + module_id, + activity_name, + description, + level, + point, + type +) => { + try { + const response = await fetch(`${URL}/domains/activity_new`, { + method: "POST", + body: JSON.stringify({ + module_id, + activity_name, + description, + level, + point, + type, + }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + return Promise.reject(new Error("Couldn't add domain")); + } + const data = await response.json(); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Couldn't add domain")); + } +}; + +const getRequestedTasksToReview = async () => { + try { + const response = await fetch(`${URL}/domains/get_requested_tasks`, { + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; +const markUserProgressModuleAsFailed = async (user_progress_module_id) => { + try { + const response = await fetch( + `${URL}/domains/mark_user_progress_module_as_failed/${user_progress_module_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const markUserProgressModuleAsPassed = async (user_progress_module_id) => { + try { + const response = await fetch( + `${URL}/domains/mark_user_progress_module_as_passed/${user_progress_module_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const getRegisteredModules = async () => { + try { + const response = await fetch(`${URL}/domains/get_registered_modules`, { + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const getModulesByInstanceId = async (instance_id) => { + try { + const response = await fetch( + `${URL}/domains/get_modules_by_instance_id/${instance_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const getInstancesByCourseId = async (course_id) => { + try { + const response = await fetch( + `${URL}/domains/get_instances_by_course_id/${course_id}`, + { + headers: { "Content-Type": "application/json" }, + credentials: "include", + } + ); + if (response.status === 200) { + const responseJson = await response.json(); + return Promise.resolve(responseJson); + } + } catch (error) { + return Promise.reject(error); + } +}; + +const createPost = async (user_progress_module_id, content) => { + try { + const response = await fetch(`${URL}/domains/create_user_progress_post`, { + method: "POST", + body: JSON.stringify({ user_progress_module_id, content }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Response:", data); + return data; + } catch (error) { + console.error("Error during request:", error); + throw error; + } +}; + +const createPostComment = async (user_post_id, comment) => { + try { + const response = await fetch(`${URL}/domains/create_userpostcomments`, { + method: "POST", + body: JSON.stringify({ user_post_id, comment }), + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Response:", data); + return Promise.resolve(data); + } catch (error) { + return Promise.reject(new Error("Error during request:", error)); + } +}; + +const community = async () => { + try { + const response = await fetch(`${URL}/domains/community`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const data = await response.json(); + console.log("Response:", data); + return data; + } catch (error) { + console.error("Error during request:", error); + throw error; + } +}; +// @domains_bp.route('/domains/analytics', methods=['GET']) +// def analytics(): +// results = DomainTable().get_analytics_data() +// return jsonify(results), 200 +const analytics = async () => { + try { + const response = await fetch(`${URL}/domains/analytics`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const data = await response.json(); + console.log("Response:", data); + return data; + } catch (error) { + console.error("Error during request:", error); + throw error; + } +}; +module.exports = { + getAllDomines, + registerToCourse, + getUserProgress, + submitModuleActivity, + getRequestedTasksToReview, + markUserProgressModuleAsFailed, + markUserProgressModuleAsPassed, + getRegisteredModules, + getModulesByInstanceId, + getInstancesByCourseId, + createPost, + createPostComment, + community, + getAllUserProgress, + getAllDomaines, + addNewActivity, + addNewCourse, + addNewDomain, + addNewInstace, + addNewModule, + analytics, +}; diff --git a/frontend/web/src/setupTests.js b/frontend/web/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/frontend/web/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/frontend/web/tailwind.config.js b/frontend/web/tailwind.config.js new file mode 100644 index 0000000..f6ca6c6 --- /dev/null +++ b/frontend/web/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + darkMode: 'class', + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..36be697 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Flask +Flask-RESTful +Flask-SQLAlchemy +SQLAlchemy +Werkzeug +marshmallow +openai +Jinja2 +requests +flask-cors