diff --git a/Dockerfile b/Dockerfile index 43be8f5..75d3a16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,4 @@ USER app EXPOSE 5000 # Run the application with Gunicorn in production mode using config file -CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"] \ No newline at end of file +# CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"] diff --git a/app.py b/app.py index c66497c..ebc4545 100644 --- a/app.py +++ b/app.py @@ -14,9 +14,13 @@ import glob import csv import uuid from datetime import datetime +import logging app = Flask(__name__) +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + # Load configuration from environment variables with defaults CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content') STATIC_DIR = os.environ.get('STATIC_DIR', 'static') @@ -141,6 +145,7 @@ def get_student_progress(token): def update_student_progress(token, lesson_name, status="completed"): """Update the progress of a student for a specific lesson""" if not os.path.exists(TOKENS_FILE): + logging.warning(f"Tokens file {TOKENS_FILE} does not exist") return False # Read all rows @@ -154,9 +159,13 @@ def update_student_progress(token, lesson_name, status="completed"): updated = False for row in rows: if row['token'] == token: + # Check if the lesson_name exists in fieldnames if lesson_name in fieldnames: row[lesson_name] = status updated = True + logging.info(f"Updating progress for token {token}, lesson {lesson_name}, status {status}") + else: + logging.warning(f"Lesson '{lesson_name}' not found in CSV columns: {fieldnames}") break # Write the updated data back to the file @@ -165,6 +174,9 @@ def update_student_progress(token, lesson_name, status="completed"): writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';') writer.writeheader() writer.writerows(rows) + logging.info(f"Updated progress for token {token}, lesson {lesson_name}, status {status}") + else: + logging.warning(f"Failed to update progress for token {token}, lesson {lesson_name}") return updated @@ -210,6 +222,153 @@ def get_ordered_lessons(): # If no specific order is defined in home.md, return lessons in default order return get_lessons() + +def get_lessons_with_learning_objectives(): + """Get all lesson files from the content directory with learning objectives as descriptions""" + lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md")) + lessons = [] + + for file_path in lesson_files: + filename = os.path.basename(file_path) + # Skip home.md as it's not a lesson + if filename == "home.md": + continue + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + # Extract title from first line if it starts with # + lines = content.split('\n') + title = "Untitled" + description = "Learn C programming concepts with practical examples." + + # Look for lesson info section to extract learning objectives + lesson_info_start = content.find('---LESSON_INFO---') + lesson_info_end = content.find('---END_LESSON_INFO---') + + if lesson_info_start != -1 and lesson_info_end != -1: + lesson_info_section = content[lesson_info_start + len('---LESSON_INFO---'):lesson_info_end] + + # Extract learning objectives + objectives_start = lesson_info_section.find('**Learning Objectives:**') + if objectives_start != -1: + objectives_section = lesson_info_section[objectives_start:] + + # Find the objectives list + import re + # Look for bullet points after Learning Objectives + objective_matches = re.findall(r'- ([^\n]+)', objectives_section) + + if objective_matches: + # Combine first few objectives as description + description = '; '.join(objective_matches[:3]) # Take first 3 objectives + else: + # If no bullet points found, take a few lines after the heading + lines_after = lesson_info_section[objectives_start:].split('\n')[1:4] + description = ' '.join(line.strip() for line in lines_after if line.strip()) + + # Look for the main title after the lesson info section + # Find the content after END_LESSON_INFO + content_after_info = content[lesson_info_end + len('---END_LESSON_INFO---'):].strip() + content_lines = content_after_info.split('\n') + + # Find the first line that starts with # (the main title) + for line in content_lines: + if line.startswith('# '): + title = line[2:].strip() + break + else: + # If no lesson info section, use the original method + for i, line in enumerate(lines): + if line.startswith('# '): + title = line[2:].strip() + break + + lessons.append({ + 'filename': filename, + 'title': title, + 'description': description, + 'path': file_path + }) + + return lessons + + +def get_ordered_lessons_with_learning_objectives(progress=None): + """Get lessons in the order specified in home.md if available with learning objectives as descriptions""" + # Read home content to check for lesson order + home_file_path = os.path.join(CONTENT_DIR, "home.md") + if os.path.exists(home_file_path): + with open(home_file_path, 'r', encoding='utf-8') as f: + home_content = f.read() + + # Split content to get only the lesson list part + parts = home_content.split('---Available_Lessons---') + if len(parts) > 1: + lesson_list_content = parts[1] + + # Find lesson links in the lesson list content + import re + # Look for markdown links that point to lessons + lesson_links = re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', lesson_list_content) + + if lesson_links: + # Create ordered list based on home.md + all_lessons = get_lessons_with_learning_objectives() + ordered_lessons = [] + + for link_text, filename in lesson_links: + for lesson in all_lessons: + if lesson['filename'] == filename: + # Update title to use the link text from home.md if needed + lesson_copy = lesson.copy() + lesson_copy['title'] = link_text + + # Add completion status if progress is provided + if progress: + lesson_key = filename.replace('.md', '') + if lesson_key in progress: + lesson_copy['completed'] = progress[lesson_key] == 'completed' + else: + lesson_copy['completed'] = False + else: + lesson_copy['completed'] = False + + ordered_lessons.append(lesson_copy) + break + + # Add any remaining lessons not mentioned in home.md + for lesson in all_lessons: + if not any(l['filename'] == lesson['filename'] for l in ordered_lessons): + # Add completion status if progress is provided + if progress: + lesson_key = lesson['filename'].replace('.md', '') + if lesson_key in progress: + lesson['completed'] = progress[lesson_key] == 'completed' + else: + lesson['completed'] = False + else: + lesson['completed'] = False + ordered_lessons.append(lesson) + + return ordered_lessons + + # If no specific order is defined in home.md, return lessons in default order + all_lessons = get_lessons_with_learning_objectives() + + # Add completion status if progress is provided + if progress: + for lesson in all_lessons: + lesson_key = lesson['filename'].replace('.md', '') + if lesson_key in progress: + lesson['completed'] = progress[lesson_key] == 'completed' + else: + lesson['completed'] = False + else: + for lesson in all_lessons: + lesson['completed'] = False + + return all_lessons + def render_markdown_content(file_path): """Render markdown content to HTML""" with open(file_path, 'r', encoding='utf-8') as f: @@ -290,7 +449,20 @@ def render_markdown_content(file_path): @app.route('/') def index(): """Main page showing all lessons""" - lessons = get_ordered_lessons() + # Get token from session or request (for now, we'll pass it in the template context) + token = request.args.get('token', '') # This would typically come from session after login + + # If no token provided in URL, try to get from cookie + if not token: + token = request.cookies.get('student_token', '') + + # Get student progress if token is provided + progress = None + if token: + progress = get_student_progress(token) + print(f"Progress for token {token}: {progress}") # Logging for debugging + + lessons = get_ordered_lessons_with_learning_objectives(progress) # Read home content from a home.md file if it exists home_content = "" @@ -304,7 +476,11 @@ def index(): main_content = parts[0] if len(parts) > 0 else full_content home_content = markdown.markdown(main_content, extensions=['fenced_code', 'codehilite', 'tables']) - return render_template('index.html', lessons=lessons, home_content=home_content) + print(f"Sending to template - token: {token}, progress: {progress}, lessons count: {len(lessons)}") # Logging for debugging + for lesson in lessons: + print(f"Lesson: {lesson['title']}, completed: {lesson.get('completed', 'N/A')}") # Logging for debugging + + return render_template('index.html', lessons=lessons, home_content=home_content, token=token, progress=progress) @app.route('/lesson/') def lesson(filename): @@ -325,6 +501,44 @@ int main() { return 0; }""" + # Get token from session or request (for now, we'll pass it in the template context) + token = request.args.get('token', '') # This would typically come from session after login + + # If no token provided in URL, try to get from cookie + if not token: + token = request.cookies.get('student_token', '') + + # Get student progress if token is provided + progress = None + lesson_completed = False + if token: + progress = get_student_progress(token) + if progress and filename.replace('.md', '') in progress: + lesson_completed = progress[filename.replace('.md', '')] == 'completed' + + # Get ordered lessons to determine next and previous lessons + all_lessons = get_ordered_lessons_with_learning_objectives(progress) + + # Find current lesson index + current_lesson_idx = -1 + for i, lesson in enumerate(all_lessons): + if lesson['filename'] == filename: + current_lesson_idx = i + break + + # Determine previous and next lessons + prev_lesson = None + next_lesson = None + + if current_lesson_idx != -1: + if current_lesson_idx > 0: + prev_lesson = all_lessons[current_lesson_idx - 1] + if current_lesson_idx < len(all_lessons) - 1: + next_lesson = all_lessons[current_lesson_idx + 1] + + # Get ordered lessons for the sidebar + ordered_lessons = get_ordered_lessons_with_learning_objectives(progress) + return render_template('lesson.html', lesson_content=lesson_html, exercise_content=exercise_html, @@ -332,7 +546,13 @@ int main() { lesson_info=lesson_info, initial_code=initial_code, solution_code=solution_code, - lesson_title=filename.replace('.md', '').replace('_', ' ').title()) + lesson_title=filename.replace('.md', '').replace('_', ' ').title(), + token=token, + progress=progress, + lesson_completed=lesson_completed, + prev_lesson=prev_lesson, + next_lesson=next_lesson, + ordered_lessons=ordered_lessons) @app.route('/compile', methods=['POST']) def compile_code(): @@ -447,17 +667,35 @@ def login(): # Validate the token student_info = validate_token(token) if student_info: - return jsonify({ + response = jsonify({ 'success': True, 'student_name': student_info['student_name'], 'message': 'Login successful' }) + # Set token in cookie with expiration + response.set_cookie('student_token', token, httponly=True, secure=False, samesite='Lax', max_age=86400) # 24 hours + return response else: return jsonify({'success': False, 'message': 'Invalid token'}) except Exception as e: return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'}) +@app.route('/logout', methods=['POST']) +def logout(): + """Handle student logout""" + try: + # Create response that clears the cookie + response = jsonify({ + 'success': True, + 'message': 'Logout successful' + }) + # Clear the student_token cookie + response.set_cookie('student_token', '', expires=0) + return response + except Exception as e: + return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'}) + @app.route('/validate-token', methods=['POST']) def validate_token_route(): """Validate a token without logging in""" @@ -465,6 +703,10 @@ def validate_token_route(): data = request.get_json() token = data.get('token', '').strip() + # If no token provided in request, try to get from cookie + if not token: + token = request.cookies.get('student_token', '').strip() + if not token: return jsonify({'success': False, 'message': 'Token is required'}) @@ -490,6 +732,8 @@ def track_progress(): lesson_name = data.get('lesson_name', '').strip() status = data.get('status', 'completed').strip() + logging.info(f"Received track-progress request: token={token}, lesson_name={lesson_name}, status={status}") + if not token or not lesson_name: return jsonify({'success': False, 'message': 'Token and lesson name are required'}) @@ -501,23 +745,31 @@ def track_progress(): # Update progress updated = update_student_progress(token, lesson_name, status) if updated: + logging.info(f"Progress updated successfully for token {token}, lesson {lesson_name}") return jsonify({ 'success': True, 'message': 'Progress updated successfully' }) else: + logging.warning(f"Failed to update progress for token {token}, lesson {lesson_name}") return jsonify({'success': False, 'message': 'Failed to update progress'}) except Exception as e: + logging.error(f"Error in track-progress: {str(e)}") return jsonify({'success': False, 'message': f'Error tracking progress: {str(e)}'}) @app.context_processor def inject_functions(): - """Make get_lessons function available in templates""" - return dict(get_lessons=get_lessons) + """Make functions available in templates""" + return dict( + get_lessons=get_lessons, + get_ordered_lessons_with_learning_objectives=get_ordered_lessons_with_learning_objectives + ) # Initialize the tokens file when the app starts initialize_tokens_file() if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False) \ No newline at end of file + import os + debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true' + app.run(host='0.0.0.0', port=5000, debug=debug_mode) \ No newline at end of file diff --git a/assets/InitilizationofaVariable-660x330.png b/example_assets/InitilizationofaVariable-660x330.png similarity index 100% rename from assets/InitilizationofaVariable-660x330.png rename to example_assets/InitilizationofaVariable-660x330.png diff --git a/generate_tokens.py b/generate_tokens.py index 49a00d9..d43738c 100644 --- a/generate_tokens.py +++ b/generate_tokens.py @@ -29,16 +29,33 @@ def get_lesson_names(): def generate_tokens_csv(): """Generate the tokens CSV file with headers and lesson columns""" lesson_names = get_lesson_names() - + # Create the file with headers headers = ['token', 'nama_siswa'] + lesson_names - + with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.writer(csvfile, delimiter=';') writer.writerow(headers) - + + # Add a dummy example row for reference + dummy_row = ['dummy_token_12345', 'Example Student'] + ['not_started'] * len(lesson_names) + writer.writerow(dummy_row) + + # Set file permissions to allow read/write access for all users + # This ensures the container can update the file when progress is tracked + try: + import stat + import os + current_permissions = os.stat(TOKENS_FILE).st_mode + os.chmod(TOKENS_FILE, current_permissions | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) + print(f"Set permissions for {TOKENS_FILE} to allow container access") + except Exception as e: + print(f"Warning: Could not set file permissions: {e}") + print(f"Created tokens file: {TOKENS_FILE} with headers: {headers}") print("Teachers can now add student tokens and names directly to this file.") + print("An example row has been added with token 'dummy_token_12345' for reference.") + print("To add new students, add new rows with format: token;nama_siswa;lesson1_status;lesson2_status;...") if __name__ == '__main__': generate_tokens_csv() diff --git a/podman-compose.yml b/podman-compose.yml index 3440daf..a6eb632 100644 --- a/podman-compose.yml +++ b/podman-compose.yml @@ -9,9 +9,16 @@ services: - ./static:/app/static - ./templates:/app/templates - ../tokens_siswa.csv:/app/tokens.csv + - ../assets:/app/assets env_file: - ../.env + + # production command: gunicorn --config gunicorn.conf.py app:app + + # debug + # command: python app.py + elemes-ts: image: docker.io/tailscale/tailscale:latest container_name: elemes-ts diff --git a/templates/index.html b/templates/index.html index ee6050d..cf13874 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,6 +18,7 @@
+
@@ -42,11 +43,22 @@
-
{{ lesson.title }}
+
+ {{ lesson.title }} + {% if progress %} + {% if lesson.completed %} + Completed + {% else %} + Pending + {% endif %} + {% endif %} +

{{ lesson.description }}

@@ -75,6 +87,125 @@ const studentTokenInput = document.getElementById('student-token'); const studentInfoDiv = document.getElementById('student-info'); + // Function to handle logout + function performLogout() { + console.log("Logout function called"); + + // Call server endpoint to clear cookie + fetch('/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log("Server logout successful"); + } else { + console.error("Server logout failed:", data.message); + } + + // Clear localStorage + localStorage.removeItem('student_token'); + + // Clear the token input field + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.value = ''; + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + console.log("Cleared token input"); + } + + // Show login button and hide logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + console.log("Showed login button"); + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + console.log("Hid logout button"); + } + + // Hide student info + studentInfoDiv.style.display = 'none'; + console.log("Hid student info"); + + // Reset form display + tokenForm.style.display = 'flex'; + console.log("Reset form display"); + + // Force a page reload to ensure everything is reset properly + location.reload(); + }) + .catch(error => { + console.error('Error during logout:', error); + + // Even if server logout fails, still clear local storage and UI + localStorage.removeItem('student_token'); + + // Clear the token input field + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.value = ''; + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + console.log("Cleared token input"); + } + + // Show login button and hide logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + console.log("Showed login button"); + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + console.log("Hid logout button"); + } + + // Hide student info + studentInfoDiv.style.display = 'none'; + console.log("Hid student info"); + + // Reset form display + tokenForm.style.display = 'flex'; + console.log("Reset form display"); + + // Force a page reload to ensure everything is reset properly + location.reload(); + }); + } + + // Function to initialize logout button + function initializeLogoutButton() { + const logoutBtn = document.querySelector('.logout-btn'); + if (logoutBtn) { + console.log("Adding event listener to logout button"); + // Remove any existing event listeners to avoid duplicates + logoutBtn.replaceWith(logoutBtn.cloneNode(true)); + const newLogoutBtn = document.querySelector('.logout-btn'); + newLogoutBtn.addEventListener('click', performLogout); + console.log("Event listener added successfully"); + } else { + console.log("Logout button not found"); + } + } + + // Add event listener to the logout button after DOM is loaded + document.addEventListener('DOMContentLoaded', function() { + console.log("DOM loaded, initializing logout button"); + initializeLogoutButton(); + }); + // Handle token form submission tokenForm.addEventListener('submit', function(e) { e.preventDefault(); @@ -86,6 +217,13 @@ return; } + // Prevent multiple submissions by disabling the submit button temporarily + const submitBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = 'Logging in...'; + } + // Send token to server for validation fetch('/login', { method: 'POST', @@ -96,6 +234,12 @@ }) .then(response => response.json()) .then(data => { + // Re-enable the submit button + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = 'Login'; + } + if (data.success) { // Store token in localStorage for persistence localStorage.setItem('student_token', token); @@ -104,35 +248,35 @@ studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.style.display = 'block'; - // Hide the form after successful login - tokenForm.style.display = 'none'; + // Hide the login input and login button, show logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'none'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'inline-block'; + logoutBtn.onclick = performLogout; + } // Update the token input to show logged in state studentTokenInput.placeholder = 'Logged in'; studentTokenInput.disabled = true; - // Add logout button - const logoutBtn = document.createElement('button'); - logoutBtn.className = 'btn btn-outline-light'; - logoutBtn.textContent = 'Logout'; - logoutBtn.type = 'button'; - logoutBtn.onclick = function() { - localStorage.removeItem('student_token'); - // Clear the token input field - const tokenInput = document.getElementById('student-token'); - if (tokenInput) { - tokenInput.value = ''; - tokenInput.placeholder = 'Enter token'; - tokenInput.disabled = false; - } - location.reload(); - }; - tokenForm.appendChild(logoutBtn); + // Redirect to homepage with token to ensure progress is loaded + window.location.href = `/?token=${token}`; } else { alert(data.message || 'Invalid token'); } }) .catch(error => { + // Re-enable the submit button + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = 'Login'; + } console.error('Error:', error); alert('An error occurred while logging in'); }); @@ -156,33 +300,60 @@ studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.style.display = 'block'; - // Hide the form since user is already logged in - tokenForm.style.display = 'none'; + // Hide the login input and login button, show logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); - // Add logout button - const logoutBtn = document.createElement('button'); - logoutBtn.className = 'btn btn-outline-light'; - logoutBtn.textContent = 'Logout'; - logoutBtn.type = 'button'; - logoutBtn.onclick = function() { - localStorage.removeItem('student_token'); - // Clear the token input field - const tokenInput = document.getElementById('student-token'); - if (tokenInput) { - tokenInput.value = ''; - tokenInput.placeholder = 'Enter token'; - tokenInput.disabled = false; - } - location.reload(); - }; - tokenForm.appendChild(logoutBtn); + if (loginBtn) { + loginBtn.style.display = 'none'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'inline-block'; + logoutBtn.onclick = performLogout; + } + + // Update the token input to show logged in state + studentTokenInput.placeholder = 'Logged in'; + studentTokenInput.disabled = true; } else { // Token is invalid, remove it from localStorage localStorage.removeItem('student_token'); + // Reset UI to logged out state + studentInfoDiv.style.display = 'none'; + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + } + + studentTokenInput.placeholder = 'Enter token'; + studentTokenInput.disabled = false; } }) .catch(error => { console.error('Error validating token:', error); + // Reset UI to logged out state if there's an error + localStorage.removeItem('student_token'); + studentInfoDiv.style.display = 'none'; + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + } + + studentTokenInput.placeholder = 'Enter token'; + studentTokenInput.disabled = false; }); } }); diff --git a/templates/lesson.html b/templates/lesson.html index 11a957c..17fa08b 100644 --- a/templates/lesson.html +++ b/templates/lesson.html @@ -38,6 +38,7 @@
+
@@ -120,8 +121,27 @@ + + +
+ {% if prev_lesson %} + + Previous: {{ prev_lesson.title }} + + {% else %} +
+ {% endif %} + + {% if next_lesson %} + + Next: {{ next_lesson.title }} + + {% else %} +
+ {% endif %} +
- +
@@ -129,6 +149,16 @@

Current Lesson: {{ lesson_title }}

+ {% if progress %} +

+ Status: + {% if lesson_completed %} + Completed + {% else %} + In Progress + {% endif %} +

+ {% endif %} {% if lesson_info %}
{{ lesson_info | safe }} @@ -138,18 +168,37 @@ {% endif %}
- +
All Lessons
- {% for lesson in get_lessons() %} - - {{ lesson.title }} - - {% endfor %} + {% if ordered_lessons %} + {% for lesson in ordered_lessons %} + + {{ lesson.title }} + {% if progress %} + {% if lesson.completed %} + + {% endif %} + {% endif %} + + {% endfor %} + {% else %} + {% for lesson in get_ordered_lessons_with_learning_objectives(progress) %} + + {{ lesson.title }} + {% if progress %} + {% if lesson.completed %} + + {% endif %} + {% endif %} + + {% endfor %} + {% endif %}
@@ -367,7 +416,14 @@ if (savedToken) { // Extract lesson name from the URL const pathParts = window.location.pathname.split('/'); - const lessonFilename = pathParts[pathParts.length - 1]; + let lessonFilename = pathParts[pathParts.length - 1]; + + // Handle the case where the URL might include query parameters + if (lessonFilename.includes('?')) { + lessonFilename = lessonFilename.split('?')[0]; + } + + // Extract just the lesson name without .md extension const lessonName = lessonFilename.replace('.md', ''); // Send progress to server @@ -385,7 +441,46 @@ .then(response => response.json()) .then(data => { if (data.success) { - console.log('Progress tracked successfully'); + console.log('Progress tracked successfully for lesson:', lessonName); + // Update the UI to reflect the new status + document.querySelectorAll('.lesson-card').forEach(card => { + const link = card.querySelector('a'); + if (link && link.href.includes(lessonFilename)) { + const statusBadge = card.querySelector('.badge'); + if (statusBadge) { + statusBadge.className = 'badge bg-success float-end'; + statusBadge.title = 'Completed'; + statusBadge.innerHTML = ' Completed'; + } + const btn = card.querySelector('.btn-primary'); + if (btn) { + btn.textContent = btn.textContent.replace('Start Learning', 'Review'); + } + } + }); + + // Update the current lesson status in the sidebar + const statusElement = document.querySelector('.card-body p strong + p span'); + if (statusElement) { + statusElement.className = 'badge bg-success'; + statusElement.innerHTML = ' Completed'; + } + + // Update the lesson in the "All Lessons" sidebar + document.querySelectorAll('.list-group-item').forEach(item => { + if (item.href && item.href.includes(lessonFilename)) { + let badge = item.querySelector('.badge'); + if (!badge) { + badge = document.createElement('span'); + badge.className = 'badge bg-success float-end'; + badge.innerHTML = ''; + item.appendChild(badge); + } else { + badge.className = 'badge bg-success float-end'; + badge.innerHTML = ''; + } + } + }); } else { console.error('Failed to track progress:', data.message); } @@ -538,30 +633,75 @@ studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.style.display = 'block'; - // Hide the form after successful login - tokenForm.style.display = 'none'; + // Hide the login input and login button, show logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'none'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'inline-block'; + logoutBtn.onclick = function() { + // Call server endpoint to clear cookie + fetch('/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log("Server logout successful"); + } else { + console.error("Server logout failed:", data.message); + } + + // Clear localStorage + localStorage.removeItem('student_token'); + // Clear the token input field + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.value = ''; + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + } + // Redirect to lesson page without token to ensure clean state + window.location.href = window.location.pathname; + }) + .catch(error => { + console.error('Error during logout:', error); + + // Even if server logout fails, still clear local storage and redirect + localStorage.removeItem('student_token'); + // Clear the token input field + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.value = ''; + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + } + // Redirect to lesson page without token to ensure clean state + window.location.href = window.location.pathname; + }); + }; + } // Update the token input to show logged in state studentTokenInput.placeholder = 'Logged in'; studentTokenInput.disabled = true; - // Add logout button - const logoutBtn = document.createElement('button'); - logoutBtn.className = 'btn btn-outline-light'; - logoutBtn.textContent = 'Logout'; - logoutBtn.type = 'button'; - logoutBtn.onclick = function() { - localStorage.removeItem('student_token'); - // Clear the token input field - const tokenInput = document.getElementById('student-token'); - if (tokenInput) { - tokenInput.value = ''; - tokenInput.placeholder = 'Enter token'; - tokenInput.disabled = false; - } - location.reload(); - }; - tokenForm.appendChild(logoutBtn); + // Redirect to current lesson page with token to ensure progress is loaded + const currentPath = window.location.pathname; + const storedToken = localStorage.getItem('student_token'); + if (storedToken) { + window.location.href = `${currentPath}?token=${storedToken}`; + } else { + console.error('Token not found in localStorage after login'); + alert('An error occurred while processing your login'); + } } else { alert(data.message || 'Invalid token'); } @@ -591,15 +731,16 @@ studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.style.display = 'block'; - // Hide the form since user is already logged in - if (tokenForm) { - tokenForm.style.display = 'none'; + // Hide the login input and login button, show logout button + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); - // Add logout button - const logoutBtn = document.createElement('button'); - logoutBtn.className = 'btn btn-outline-light'; - logoutBtn.textContent = 'Logout'; - logoutBtn.type = 'button'; + if (loginBtn) { + loginBtn.style.display = 'none'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'inline-block'; logoutBtn.onclick = function() { localStorage.removeItem('student_token'); // Clear the token input field @@ -609,17 +750,54 @@ tokenInput.placeholder = 'Enter token'; tokenInput.disabled = false; } - location.reload(); + // Redirect to lesson page without token to ensure clean state + window.location.href = window.location.pathname; }; - tokenForm.appendChild(logoutBtn); } } else { // Token is invalid, remove it from localStorage localStorage.removeItem('student_token'); + // Reset UI to logged out state + studentInfoDiv.style.display = 'none'; + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + } + + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + } } }) .catch(error => { console.error('Error validating token:', error); + // Reset UI to logged out state if there's an error + localStorage.removeItem('student_token'); + studentInfoDiv.style.display = 'none'; + const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)'); + const logoutBtn = tokenForm.querySelector('.logout-btn'); + + if (loginBtn) { + loginBtn.style.display = 'inline-block'; + } + + if (logoutBtn) { + logoutBtn.style.display = 'none'; + } + + const tokenInput = document.getElementById('student-token'); + if (tokenInput) { + tokenInput.placeholder = 'Enter token'; + tokenInput.disabled = false; + } }); } });