1202 lines
63 KiB
HTML
1202 lines
63 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 }};
|
|
const keyText = {{ key_text | 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;
|
|
}
|
|
|
|
// Perform static analysis first
|
|
let staticAnalysisPassed = true;
|
|
let staticAnalysisMessage = '';
|
|
|
|
if (keyText && keyText !== "None" && keyText !== "") {
|
|
// Parse the key text to get required keywords
|
|
const requiredKeywords = keyText.split('\n').filter(keyword => keyword.trim() !== '');
|
|
|
|
// Perform static analysis
|
|
staticAnalysisPassed = true;
|
|
staticAnalysisMessage = '';
|
|
|
|
for (const keyword of requiredKeywords) {
|
|
if (keyword.trim() !== '') {
|
|
// Check if the keyword contains regex patterns (enclosed in slashes)
|
|
const trimmedKeyword = keyword.trim();
|
|
|
|
if (trimmedKeyword.startsWith('/') && trimmedKeyword.endsWith('/')) {
|
|
// This is a regex pattern
|
|
try {
|
|
const regexPattern = trimmedKeyword.slice(1, -1); // Remove the leading and trailing slashes
|
|
const regex = new RegExp(regexPattern, 'g');
|
|
|
|
if (!regex.test(code)) {
|
|
staticAnalysisPassed = false;
|
|
staticAnalysisMessage += `Missing pattern: "${trimmedKeyword}"\n`;
|
|
}
|
|
} catch (e) {
|
|
// If regex is malformed, treat as literal string
|
|
if (!code.includes(trimmedKeyword)) {
|
|
staticAnalysisPassed = false;
|
|
staticAnalysisMessage += `Missing required keyword: "${trimmedKeyword}"\n`;
|
|
}
|
|
}
|
|
} else {
|
|
// Simple check: see if the literal keyword exists in the code
|
|
if (!code.includes(trimmedKeyword)) {
|
|
staticAnalysisPassed = false;
|
|
staticAnalysisMessage += `Missing required keyword: "${trimmedKeyword}"\n`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) AND static analysis passed
|
|
if (expectedOutput && expectedOutput !== "None" && expectedOutput !== "") {
|
|
const actualOutput = data.output || '';
|
|
|
|
// Both output match and static analysis pass are required for success
|
|
if (actualOutput.trim() === expectedOutput.trim() && staticAnalysisPassed) {
|
|
// 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 OR static analysis failed
|
|
const successElement = document.getElementById('success-message');
|
|
if (successElement) {
|
|
successElement.classList.add('d-none');
|
|
}
|
|
|
|
// Hide solution button if output doesn't match OR static analysis failed
|
|
if (solutionButton) {
|
|
solutionButton.classList.add('d-none');
|
|
}
|
|
}
|
|
} else {
|
|
// For lessons without expected output, check only static analysis
|
|
if (staticAnalysisPassed) {
|
|
// 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 static analysis failed
|
|
const successElement = document.getElementById('success-message');
|
|
if (successElement) {
|
|
successElement.classList.add('d-none');
|
|
}
|
|
|
|
// Hide solution button if static analysis failed
|
|
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> |