elemes/templates/lesson.html

1055 lines
54 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ lesson_title }} - {{ page_title_suffix }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
<link rel="stylesheet" href="{{ url_for('send_static', path='style.css') }}">
<style>
.hljs {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-code"></i> {{ app_bar_title }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<div class="d-flex" id="token-section">
<form class="d-flex" id="token-form" style="display: flex; align-items: center;">
<input class="form-control me-2" type="text" id="student-token" placeholder="Enter token" style="width: 200px;">
<button class="btn btn-outline-light" type="submit">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</button>
</form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
{% if token %}
<a href="/progress-report?token={{ token }}" class="btn btn-outline-light ms-2">
<i class="fas fa-chart-bar"></i> Progress Report
</a>
{% endif %}
</div>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-8">
<div class="lesson-content">
{{ lesson_content | safe }}
</div>
<div class="exercise-section mt-5">
<h3><i class="fas fa-laptop-code"></i> Exercise</h3>
{% if exercise_content %}
<div class="exercise-content">
{{ exercise_content | safe }}
</div>
{% else %}
<div class="exercise-content bg-light p-3 rounded">
<p class="mb-0"><em>No specific exercise for this lesson, but you can practice writing C code below.</em></p>
</div>
{% endif %}
<div class="editor-section mt-4">
<h4><i class="fas fa-code"></i> Code Editor</h4>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center" style="background-color: #4a76a8; color: white;">
<span><i class="fas fa-file-code me-2"></i>{{ language_display_name }} code.{{ 'py' if language == 'python' else 'c' }}</span>
<div>
<button id="theme-toggle" class="btn btn-sm btn-outline-light me-2">
<i class="fas fa-moon"></i> Dark
</button>
<button id="solution-code" class="btn btn-sm btn-outline-light me-2 d-none">
<i class="fas fa-lightbulb"></i> Show Solution
</button>
<button id="run-code" class="btn btn-sm btn-success me-2">
<i class="fas fa-play"></i> Run
</button>
<button id="reset-code" class="btn btn-sm btn-outline-light">
<i class="fas fa-redo"></i> Reset
</button>
</div>
</div>
<div class="card-body p-0">
<div class="code-editor-container">
<div id="editor-wrapper" class="editor-wrapper" style="position: relative; height: 300px; display: flex;">
<div id="line-numbers" class="line-numbers" style="width: 50px; text-align: right; padding: 10px 5px; overflow: hidden; user-select: none; flex-shrink: 0;">
1
</div>
<div style="flex: 1; position: relative;">
<textarea id="code-editor"
style="height: 100%; width: 100%; padding: 10px; font-family: monospace; resize: none; border: none; outline: none; tab-size: 4;"
placeholder="Type your C code here..."></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Success message card -->
<div id="success-message" class="alert alert-success d-none mt-3" role="alert">
<i class="fas fa-check-circle me-2"></i>
<strong>Tugas anda berhasil!</strong> Output sesuai dengan yang diharapkan.
</div>
<!-- Warning message card for copy-paste detection -->
<div id="warning-message" class="alert alert-danger d-none mt-3" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<span id="warning-message-text">Peringatan: Copy-paste tidak diperbolehkan. Silakan ketik kode Anda secara manual.</span>
</div>
<div id="output" class="mt-3 p-3 border rounded bg-light d-none" style="max-height: 200px; overflow-y: auto;">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0"><i class="fas fa-terminal"></i> Output</h5>
<button id="clear-output" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-eraser"></i> Clear
</button>
</div>
<pre id="output-content" class="mb-0"></pre>
</div>
</div>
</div>
<!-- Navigation buttons for next and previous lessons -->
<div class="d-flex justify-content-between mt-4">
{% if prev_lesson %}
<a href="{{ url_for('lesson', filename=prev_lesson.filename) }}{% if token %}?token={{ token }}{% endif %}" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> Previous: {{ prev_lesson.title }}
</a>
{% else %}
<div></div> <!-- Empty div to maintain spacing when there's no previous lesson -->
{% endif %}
{% if next_lesson %}
<a href="{{ url_for('lesson', filename=next_lesson.filename) }}{% if token %}?token={{ token }}{% endif %}" class="btn btn-primary">
Next: {{ next_lesson.title }} <i class="fas fa-arrow-right"></i>
</a>
{% else %}
<div></div> <!-- Empty div to maintain spacing when there's no next lesson -->
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-info-circle"></i> Lesson Information</h5>
</div>
<div class="card-body">
<p><strong>Current Lesson:</strong> {{ lesson_title }}</p>
{% if progress %}
<p>
<strong>Status:</strong>
{% if lesson_completed %}
<span class="badge bg-success"><i class="fas fa-check-circle"></i> Completed</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-clock"></i> In Progress</span>
{% endif %}
</p>
{% endif %}
{% if lesson_info %}
<div class="lesson-info-content">
{{ lesson_info | safe }}
</div>
{% else %}
<p class="text-muted">Complete the exercise to practice what you've learned.</p>
{% endif %}
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-list"></i> All Lessons</h5>
</div>
<div class="list-group list-group-flush">
{% if ordered_lessons %}
{% for lesson in ordered_lessons %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}{% if token %}?token={{ token }}{% endif %}"
class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }}
{% if progress %}
{% if lesson.completed %}
<span class="badge bg-success float-end"><i class="fas fa-check-circle"></i></span>
{% endif %}
{% endif %}
</a>
{% endfor %}
{% else %}
{% for lesson in get_ordered_lessons_with_learning_objectives(progress) %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}{% if token %}?token={{ token }}{% endif %}"
class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }}
{% if progress %}
{% if lesson.completed %}
<span class="badge bg-success float-end"><i class="fas fa-check-circle"></i></span>
{% endif %}
{% endif %}
</a>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
<footer class="footer mt-5 py-4 bg-light">
<div class="container text-center">
<span class="text-muted">{{ copyright_text }}</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Get initial code from the template
const initialCode = {{ initial_code | tojson }};
const solutionCode = {{ solution_code | tojson }};
const expectedOutput = {{ expected_output | tojson }};
// Get DOM elements
const runButton = document.getElementById('run-code');
const resetButton = document.getElementById('reset-code');
const clearOutputButton = document.getElementById('clear-output');
const outputDiv = document.getElementById('output');
const outputContent = document.getElementById('output-content');
const solutionButton = document.getElementById('solution-code');
const themeToggle = document.getElementById('theme-toggle');
const codeEditor = document.getElementById('code-editor');
// Set initial code
codeEditor.value = initialCode;
// Check for saved theme preference
const savedTheme = localStorage.getItem('editor-theme');
let isDarkTheme = savedTheme === 'dark';
// Get the line numbers container (now part of HTML)
const lineNumbersContainer = document.getElementById('line-numbers');
// Function to apply dark theme
function applyDarkTheme() {
codeEditor.classList.add('code-editor-dark');
outputDiv.classList.add('output-dark');
// Update line numbers for dark theme
lineNumbersContainer.style.backgroundColor = '#3e3d32';
lineNumbersContainer.style.color = '#75715e';
lineNumbersContainer.style.borderColor = '#49483e';
codeEditor.style.backgroundColor = '#272822';
codeEditor.style.color = '#f8f8f2';
}
// Function to apply light theme
function applyLightTheme() {
codeEditor.classList.remove('code-editor-dark');
outputDiv.classList.remove('output-dark');
// Update line numbers for light theme
lineNumbersContainer.style.backgroundColor = '#eef0f3';
lineNumbersContainer.style.color = '#6c757d';
lineNumbersContainer.style.borderColor = '#dee2e6';
codeEditor.style.backgroundColor = 'white';
codeEditor.style.color = 'black';
}
// Apply theme based on preference
if (isDarkTheme) {
applyDarkTheme();
themeToggle.innerHTML = '<i class="fas fa-sun"></i> Light';
} else {
applyLightTheme();
themeToggle.innerHTML = '<i class="fas fa-moon"></i> Dark';
}
// Function to update line numbers
function updateLineNumbers() {
const lines = codeEditor.value.split('\n');
const lineCount = lines.length || 1; // Ensure at least 1 line
let lineNumbersHTML = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHTML += i + '<br>';
}
lineNumbersContainer.innerHTML = lineNumbersHTML;
}
// Initialize line numbers
updateLineNumbers();
// Update line numbers when content changes
codeEditor.addEventListener('input', updateLineNumbers);
codeEditor.addEventListener('scroll', function() {
lineNumbersContainer.scrollTop = codeEditor.scrollTop;
});
// Update line numbers on other events that might change content
codeEditor.addEventListener('keyup', updateLineNumbers);
// Proper ClipboardEvent API implementation
// Store initial code to restore when paste is detected
let originalCodeOnFocus = initialCode;
// Store the code when editor gains focus
codeEditor.addEventListener('focus', function() {
originalCodeOnFocus = codeEditor.value;
});
// Prevent paste event using ClipboardEvent API
codeEditor.addEventListener('paste', function(e) {
// Store the current value before paste
const currentValue = codeEditor.value;
// Prevent the default paste behavior
e.preventDefault();
e.stopPropagation();
// Use a timeout to ensure the paste operation is fully processed
setTimeout(() => {
// Check if the content has changed (some paste operations might bypass preventDefault)
if (codeEditor.value !== currentValue) {
// Restore the original code before the paste attempt
codeEditor.value = currentValue;
updateLineNumbers();
}
// Show warning notification
showCopyPasteNotification('Paste detected and blocked. Copy-paste is not allowed in the code editor. Please type your code manually.');
}, 10); // Small delay to allow paste to potentially occur
return false;
});
// Prevent copy event using ClipboardEvent API
codeEditor.addEventListener('copy', function(e) {
e.preventDefault();
e.stopPropagation();
// Access the selected text to provide feedback
const selectedText = window.getSelection ? window.getSelection().toString() : '';
if (selectedText && selectedText.trim() !== '') {
showCopyPasteNotification('Copy detected. Copy is not allowed in the code editor. Please type your code manually.');
}
});
// Prevent cut event using ClipboardEvent API
codeEditor.addEventListener('cut', function(e) {
e.preventDefault();
e.stopPropagation();
// Access the selected text to provide feedback
const selectedText = window.getSelection ? window.getSelection().toString() : '';
if (selectedText && selectedText.trim() !== '') {
showCopyPasteNotification('Cut detected. Cut is not allowed in the code editor. Please type your code manually.');
}
});
// Additional prevention using onpaste attribute
codeEditor.onpaste = function(e) {
e.preventDefault();
e.stopPropagation();
showCopyPasteNotification('Paste detected. Copy-paste is not allowed in the code editor. Please type your code manually.');
return false;
};
// Additional prevention using oncopy attribute
codeEditor.oncopy = function(e) {
e.preventDefault();
e.stopPropagation();
showCopyPasteNotification('Copy detected. Copy is not allowed in the code editor. Please type your code manually.');
};
// Additional prevention using oncut attribute
codeEditor.oncut = function(e) {
e.preventDefault();
e.stopPropagation();
showCopyPasteNotification('Cut detected. Cut is not allowed in the code editor. Please type your code manually.');
};
// Prevent context menu (right-click) on the code editor to avoid paste options
codeEditor.addEventListener('contextmenu', function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
// Device detection function - updated to consider screen size as well
function isMobileDevice() {
// Check both user agent and screen size to determine if it's a mobile device
const isUserAgentMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Also check screen dimensions - if screen width is small, likely mobile device
const isScreenSmall = window.innerWidth <= 768 || window.screen.width <= 768;
// Return true if either condition is met
return isUserAgentMobile || isScreenSmall;
}
// Function to show a temporary warning card about copy-paste detection
function showCopyPasteNotification(message) {
const warningElement = document.getElementById('warning-message');
const warningTextElement = document.getElementById('warning-message-text');
if (warningElement && warningTextElement) {
warningTextElement.textContent = message;
warningElement.classList.remove('d-none');
// Hide warning after 5 seconds
setTimeout(() => {
warningElement.classList.add('d-none');
}, 5000);
}
}
// Enhanced mobile clipboard detection algorithm
// Reference: Based on techniques to detect rapid input to distinguish typing from paste
// Technique: Track timestamps of input events to detect rapid pasting (especially from GBoard)
let lastInputTime = Date.now();
const inputTimeThreshold = 100; // milliseconds - inputs faster than this threshold suggest paste
let rapidInputs = 0;
const rapidInputThreshold = 3; // number of rapid inputs to trigger detection
// Track previous value to detect large changes that might indicate pasting
let previousValue = codeEditor.value;
// Listen for input events to detect rapid typing that might indicate pasting
let lastValidValue = initialCode; // Track the last valid (non-pasted) value
codeEditor.addEventListener('input', function(e) {
const currentTime = Date.now();
const timeDiff = currentTime - lastInputTime;
// Calculate how much text was added
const currentValue = codeEditor.value;
const addedText = currentValue.length - previousValue.length;
// On mobile devices, check for both rapid inputs and large text additions
if (isMobileDevice()) {
// Check for rapid inputs - technique to detect paste events via typing speed
if (timeDiff < inputTimeThreshold) {
rapidInputs++;
if (rapidInputs >= rapidInputThreshold) {
// Likely a paste or snippet insertion on mobile (reference: detect rapid input to distinguish typing from paste)
// Restore to the last known valid value
codeEditor.value = lastValidValue;
updateLineNumbers();
showCopyPasteNotification('Rapid input detected and removed. Please type your code manually.');
rapidInputs = 0; // Reset counter
}
} else {
// Reset counter if normal typing pace resumes
rapidInputs = 0;
}
// Check for large text additions (another sign of paste)
if (addedText > 10) { // If more than 10 characters were added at once
// Restore to the last known valid value
codeEditor.value = lastValidValue;
updateLineNumbers();
showCopyPasteNotification('Large text addition detected and removed. Please type your code manually.');
}
}
// Update tracking variables
lastInputTime = currentTime;
previousValue = currentValue;
// Update the last valid value (but only if we're not in a rapid input situation)
if (isMobileDevice() && rapidInputs === 0 && addedText <= 10) {
// Consider it a valid manual input if it's not rapid or large
lastValidValue = currentValue;
} else if (!isMobileDevice()) {
// For non-mobile, always update the valid value
lastValidValue = currentValue;
}
});
// Additional detection for composition events (used by many mobile keyboards)
// Reference: Some mobile keyboards use composition events when inserting text from suggestions/clipboard
codeEditor.addEventListener('compositionstart', function(e) {
if (isMobileDevice()) {
// Store the current value before composition starts
const beforeCompositionValue = codeEditor.value;
// Some keyboards use composition events when inserting text from suggestions/clipboard
setTimeout(() => {
const currentValue = codeEditor.value;
const addedText = currentValue.length - beforeCompositionValue.length;
if (addedText > 5) { // If more than 5 characters were added via composition
// Restore to the last known valid value
codeEditor.value = lastValidValue;
updateLineNumbers();
showCopyPasteNotification('Text composition detected and removed. Please type your code manually.');
}
previousValue = codeEditor.value; // Update with current value after potential restoration
}, 10); // Small delay to allow composition to complete
}
});
codeEditor.addEventListener('copy', function(e) {
e.preventDefault();
e.stopPropagation();
alert('Copy is not allowed in the code editor. Please type your code manually.');
});
codeEditor.addEventListener('cut', function(e) {
e.preventDefault();
e.stopPropagation();
alert('Cut is not allowed in the code editor. Please type your code manually.');
});
// Prevent right-click context menu
codeEditor.addEventListener('contextmenu', function(e) {
e.preventDefault();
e.stopPropagation();
alert('Copy-paste is not allowed in the code editor. Please type your code manually.');
});
// Add keyboard shortcut for running code (Ctrl+Enter)
codeEditor.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
runButton.click();
}
// Handle Tab key to insert 4 spaces instead of changing focus
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
// Insert 4 spaces at cursor position
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
// Move cursor to after the inserted spaces
this.selectionStart = this.selectionEnd = start + 4;
// Update line numbers after tab
setTimeout(updateLineNumbers, 1);
}
});
// Run code functionality
runButton.addEventListener('click', function() {
const code = codeEditor.value;
if (!code.trim()) {
alert('Please enter some code to run.');
return;
}
// Show loading state
runButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Running...';
runButton.disabled = true;
// Get language from environment/config - default to C for backward compatibility
const language = '{{ language | default("c") }}'; // This would be passed from the backend
// Prepare data to send to the server
const requestData = {
code: code,
language: language
};
fetch('/compile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
outputDiv.classList.remove('d-none');
if (data.success) {
outputContent.textContent = data.output || 'Program executed successfully with no output.';
outputContent.className = 'output-success';
// Check if output matches expected output (if available)
if (expectedOutput && expectedOutput !== "None" && expectedOutput !== "") {
const actualOutput = data.output || '';
if (actualOutput.trim() === expectedOutput.trim()) {
// Show success message card if element exists
const successElement = document.getElementById('success-message');
if (successElement) {
// Remove d-none to show the element
successElement.classList.remove('d-none');
// Hide the success message after 10 seconds
setTimeout(function() {
successElement.classList.add('d-none');
}, 10000);
}
// Show solution button if solution code exists
if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") {
solutionButton.classList.remove('d-none');
}
// Track progress if student is logged in
const savedToken = localStorage.getItem('student_token');
if (savedToken) {
// Extract lesson name from the URL
const pathParts = window.location.pathname.split('/');
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
fetch('/track-progress', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: savedToken,
lesson_name: lessonName,
status: 'completed'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
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 = '<i class="fas fa-check-circle"></i> 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 = '<i class="fas fa-check-circle"></i> 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 = '<i class="fas fa-check-circle"></i>';
item.appendChild(badge);
} else {
badge.className = 'badge bg-success float-end';
badge.innerHTML = '<i class="fas fa-check-circle"></i>';
}
}
});
} else {
console.error('Failed to track progress:', data.message);
}
})
.catch(error => {
console.error('Error tracking progress:', error);
});
}
} else {
// Hide success message if output doesn't match
const successElement = document.getElementById('success-message');
if (successElement) {
successElement.classList.add('d-none');
}
// Hide solution button if output doesn't match
if (solutionButton) {
solutionButton.classList.add('d-none');
}
}
} else {
// Hide success message if no expected output
const successElement = document.getElementById('success-message');
if (successElement) {
successElement.classList.add('d-none');
}
// Hide solution button if no expected output
if (solutionButton) {
solutionButton.classList.add('d-none');
}
}
} else {
outputContent.textContent = data.error || data.output || 'An error occurred.';
outputContent.className = 'output-error';
// Hide success message if there's an error
const successElement = document.getElementById('success-message');
if (successElement) {
successElement.classList.add('d-none');
}
}
})
.catch(error => {
outputDiv.classList.remove('d-none');
outputContent.textContent = 'An error occurred while running the code: ' + error.message;
outputContent.className = 'output-error';
// Hide success message if there's an error
const successElement = document.getElementById('success-message');
if (successElement) {
successElement.classList.add('d-none');
}
})
.finally(() => {
// Reset button state
runButton.innerHTML = '<i class="fas fa-play"></i> Run';
runButton.disabled = false;
});
});
// Reset code functionality
resetButton.addEventListener('click', function() {
codeEditor.value = initialCode;
updateLineNumbers();
});
// Clear output functionality
clearOutputButton.addEventListener('click', function() {
outputDiv.classList.add('d-none');
});
// Add solution button functionality
if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") {
solutionButton.addEventListener('click', function() {
codeEditor.value = solutionCode;
updateLineNumbers();
});
}
// Function to apply dark theme
function applyDarkTheme() {
codeEditor.classList.add('code-editor-dark');
outputDiv.classList.add('output-dark');
// Update line numbers for dark theme
lineNumbersContainer.style.backgroundColor = '#3e3d32';
lineNumbersContainer.style.color = '#75715e';
codeEditor.style.backgroundColor = '#272822';
codeEditor.style.color = '#f8f8f2';
}
// Function to apply light theme
function applyLightTheme() {
codeEditor.classList.remove('code-editor-dark');
outputDiv.classList.remove('output-dark');
// Update line numbers for light theme
lineNumbersContainer.style.backgroundColor = '#eef0f3';
lineNumbersContainer.style.color = '#6c757d';
codeEditor.style.backgroundColor = 'white';
codeEditor.style.color = 'black';
}
// Theme toggle functionality
themeToggle.addEventListener('click', function() {
isDarkTheme = !isDarkTheme;
if (isDarkTheme) {
applyDarkTheme();
themeToggle.innerHTML = '<i class="fas fa-sun"></i> Light';
localStorage.setItem('editor-theme', 'dark');
} else {
applyLightTheme();
themeToggle.innerHTML = '<i class="fas fa-moon"></i> Dark';
localStorage.setItem('editor-theme', 'light');
}
});
// Token login functionality
const tokenForm = document.getElementById('token-form');
const studentTokenInput = document.getElementById('student-token');
const studentInfoDiv = document.getElementById('student-info');
// Handle token form submission
if (tokenForm) {
tokenForm.addEventListener('submit', function(e) {
e.preventDefault();
const token = studentTokenInput.value.trim();
if (!token) {
alert('Please enter a token');
return;
}
// Send token to server for validation
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: token })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Store token in localStorage for persistence
localStorage.setItem('student_token', token);
// Update UI to show student info
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// 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;
// 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');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while logging in');
});
});
}
// Check if user is already logged in
const savedToken = localStorage.getItem('student_token');
if (savedToken) {
// Validate the saved token
fetch('/validate-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: savedToken })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update UI to show student info
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// 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() {
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;
};
}
} 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;
}
});
}
// Dynamic image sizing and zoom functionality
function setupImageHandling() {
// Select all images inside lesson content
const lessonImages = document.querySelectorAll('.lesson-content img');
lessonImages.forEach(img => {
// Add zoomable-img class to make them zoomable
if (!img.classList.contains('zoomable-img')) {
img.classList.add('zoomable-img');
}
// Add load event to handle image sizing
if (!img.complete) {
img.addEventListener('load', function() {
// Image loaded, ensure it fits container
this.style.maxWidth = '100%';
this.style.height = 'auto';
});
} else {
// Image already loaded
img.style.maxWidth = '100%';
img.style.height = 'auto';
}
img.addEventListener('click', function() {
// Toggle enlarged class to zoom in/out
this.classList.toggle('enlarged');
// Prevent default behavior to avoid any conflicts
if (event) {
event.preventDefault();
event.stopPropagation();
}
});
});
// Add event listener to close enlarged images when clicking outside
document.addEventListener('click', function(event) {
const enlargedImg = document.querySelector('.lesson-content img.enlarged');
if (enlargedImg && !event.target.classList.contains('zoomable-img')) {
enlargedImg.classList.remove('enlarged');
}
});
}
// Call the function to set up image handling
setupImageHandling();
});
</script>
</body>
</html>