532 lines
28 KiB
HTML
532 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Générateur d'Exercices de Mathématiques</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
.card {
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
transition: 0.3s;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card:hover {
|
|
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
|
}
|
|
.exercise-result {
|
|
background-color: #f8f9fa;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
.feature-icon {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
margin-right: 0.75rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container mt-5 mb-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<div class="text-center mb-5">
|
|
<h1 class="display-4 fw-bold">Générateur d'Exercices de Mathématiques</h1>
|
|
<p class="lead">Créez des exercices de multiplication et division personnalisés en PDF</p>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Paramètres des Exercices</h5>
|
|
<form id="exerciseForm">
|
|
<div class="mb-3">
|
|
<label for="minTable" class="form-label">Table minimale</label>
|
|
<input type="number" class="form-control" id="minTable" min="1" max="12" value="1" required>
|
|
<div class="form-text">La plus petite table de multiplication à inclure</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="maxTable" class="form-label">Table maximale</label>
|
|
<input type="number" class="form-control" id="maxTable" min="1" max="12" value="10" required>
|
|
<div class="form-text">La plus grande table de multiplication à inclure</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="numExercises" class="form-label">Nombre d'exercices</label>
|
|
<input type="number" class="form-control" id="numExercises" min="1" max="100" value="15" required>
|
|
<div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div>
|
|
</div>
|
|
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary btn-lg" id="generateBtn">
|
|
<span id="buttonText">Générer le PDF</span>
|
|
<span id="spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Existing PDFs Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="card-title mb-0">Fichiers PDF Existants</h5>
|
|
<button id="refreshListBtn" class="btn btn-outline-primary btn-sm">
|
|
<span id="refreshText">Actualiser</span>
|
|
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
<div class="d-flex gap-2 mb-3">
|
|
<button id="downloadSelectedBtn" class="btn btn-success btn-sm" disabled>
|
|
Télécharger Sélectionnés
|
|
</button>
|
|
<button id="deleteSelectedBtn" class="btn btn-danger btn-sm" disabled>
|
|
Supprimer Sélectionnés
|
|
</button>
|
|
<button id="selectAllBtn" class="btn btn-outline-secondary btn-sm">
|
|
Tout Sélectionner
|
|
</button>
|
|
<button id="clearSelectionBtn" class="btn btn-outline-secondary btn-sm" disabled>
|
|
Désélectionner
|
|
</button>
|
|
</div>
|
|
<div id="pdfListContainer" class="mt-3">
|
|
<p class="text-muted">Cliquez sur "Actualiser" pour charger la liste des fichiers existants.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="resultContainer" class="exercise-result d-none">
|
|
<h5>Résultat</h5>
|
|
<div id="resultMessage"></div>
|
|
<div id="downloadLinkContainer" class="mt-3 d-none">
|
|
<a id="downloadLink" class="btn btn-success" href="#" download>Télécharger le PDF</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-5">
|
|
<div class="col-md-6">
|
|
<div class="d-flex align-items-start">
|
|
<div class="feature-icon bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-x-lg text-primary" viewBox="0 0 16 16">
|
|
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h5 class="fw-bold">Multiplications</h5>
|
|
<p>Exercices de multiplication avec des opérateurs aléatoires de la table spécifiée.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="d-flex align-items-start">
|
|
<div class="feature-icon bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-slash-lg text-success" viewBox="0 0 16 16">
|
|
<path d="M3.5 1.5l9 13"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h5 class="fw-bold">Divisions</h5>
|
|
<p>Exercices de division correspondants aux tables de multiplication sélectionnées.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-3">
|
|
<div class="col-md-6">
|
|
<div class="d-flex align-items-start">
|
|
<div class="feature-icon bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-pdf text-info" viewBox="0 0 16 16">
|
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h5 class="fw-bold">Format PDF</h5>
|
|
<p>Générez des fichiers PDF prêts à imprimer avec une mise en page organisée.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="d-flex align-items-start">
|
|
<div class="feature-icon bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-columns-gap text-warning" viewBox="0 0 16 16">
|
|
<path d="M6 1H1v3h5V1zM1 0a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1zm14 12h-5v3h5v-3zm-5-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-5zM6 8H1v7h5V8zm-1 1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H5zm9-9h-5v5h5V1zm-1 1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-5z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h5 class="fw-bold">Trois Colonnes</h5>
|
|
<p>Les exercices sont présentés en trois colonnes pour économiser du papier.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="bg-light text-center text-muted py-4 mt-5">
|
|
<div class="container">
|
|
<p>Générateur d'Exercices de Mathématiques © 2025</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// Current page for pagination
|
|
let currentPage = 1;
|
|
const pageSize = 10;
|
|
|
|
// Function to load and display existing PDFs with pagination (sorted from newest to oldest)
|
|
async function loadPdfList(page = 1) {
|
|
const refreshBtn = document.getElementById('refreshListBtn');
|
|
const refreshText = document.getElementById('refreshText');
|
|
const refreshSpinner = document.getElementById('refreshSpinner');
|
|
const pdfListContainer = document.getElementById('pdfListContainer');
|
|
|
|
// Show loading state
|
|
refreshText.textContent = 'Chargement...';
|
|
refreshSpinner.classList.remove('d-none');
|
|
refreshBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`/list?page=${page}&page_size=${pageSize}`);
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
pdfListContainer.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
|
} else {
|
|
if (result.files.length === 0) {
|
|
pdfListContainer.innerHTML = '<p class="text-muted">Aucun fichier PDF trouvé.</p>';
|
|
} else {
|
|
let html = '<div class="table-responsive"><table class="table table-striped table-hover">';
|
|
html += '<thead><tr><th><input type="checkbox" id="selectAllHeader"></th><th>Nom du fichier</th><th>Date de création</th><th>Taille</th><th>Actions</th></tr></thead><tbody>';
|
|
|
|
result.files.forEach(file => {
|
|
// Format date
|
|
const date = new Date(file.LastModified);
|
|
const formattedDate = date.toLocaleDateString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
|
|
// Format size
|
|
const sizeInKB = Math.round(file.Size / 1024);
|
|
|
|
html += '<tr>';
|
|
html += `<td><input type="checkbox" class="file-checkbox" data-filename="${file.Key}"></td>`;
|
|
html += `<td>${file.Key}</td>`;
|
|
html += `<td>${formattedDate}</td>`;
|
|
html += `<td>${sizeInKB} KB</td>`;
|
|
html += `<td>
|
|
<a href="/download/${encodeURIComponent(file.Key)}" class="btn btn-success btn-sm me-2">Télécharger</a>
|
|
<button class="btn btn-danger btn-sm delete-btn" data-filename="${file.Key}">Supprimer</button>
|
|
</td>`;
|
|
html += '</tr>';
|
|
});
|
|
|
|
html += '</tbody></table></div>';
|
|
|
|
// Add pagination controls if needed
|
|
if (result.pagination.total_pages > 1) {
|
|
html += '<nav aria-label="Pagination des fichiers"><ul class="pagination justify-content-center">';
|
|
|
|
// Previous button
|
|
if (result.pagination.has_prev) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page - 1}">Précédent</a></li>`;
|
|
} else {
|
|
html += '<li class="page-item disabled"><span class="page-link">Précédent</span></li>';
|
|
}
|
|
|
|
// Page numbers
|
|
for (let i = 1; i <= result.pagination.total_pages; i++) {
|
|
if (i === result.pagination.current_page) {
|
|
html += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
|
|
} else {
|
|
html += `<li class="page-item"><a class="page-link" href="#" data-page="${i}">${i}</a></li>`;
|
|
}
|
|
}
|
|
|
|
// Next button
|
|
if (result.pagination.has_next) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page + 1}">Suivant</a></li>`;
|
|
} else {
|
|
html += '<li class="page-item disabled"><span class="page-link">Suivant</span></li>';
|
|
}
|
|
|
|
html += '</ul></nav>';
|
|
|
|
// Display total count
|
|
html += `<div class="text-center text-muted small">
|
|
Affichage ${(result.pagination.current_page - 1) * pageSize + 1}-${Math.min(result.pagination.current_page * pageSize, result.pagination.total_files)}
|
|
sur ${result.pagination.total_files} fichiers
|
|
</div>`;
|
|
}
|
|
|
|
pdfListContainer.innerHTML = html;
|
|
|
|
// Add event listeners to pagination links
|
|
document.querySelectorAll('.page-link[data-page]').forEach(link => {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const page = parseInt(this.getAttribute('data-page'));
|
|
loadPdfList(page);
|
|
});
|
|
});
|
|
|
|
// Add event listeners to delete buttons
|
|
document.querySelectorAll('.delete-btn').forEach(button => {
|
|
button.addEventListener('click', async function() {
|
|
const filename = this.getAttribute('data-filename');
|
|
if (confirm(`Êtes-vous sûr de vouloir supprimer le fichier "${filename}" ?`)) {
|
|
try {
|
|
const response = await fetch(`/delete/${encodeURIComponent(filename)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
alert(`Erreur: ${result.error}`);
|
|
} else {
|
|
// Refresh the list (stay on current page)
|
|
loadPdfList(currentPage);
|
|
alert(result.message);
|
|
}
|
|
} catch (error) {
|
|
alert(`Erreur lors de la suppression: ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add event listener for select all checkbox in header
|
|
document.getElementById('selectAllHeader').addEventListener('change', function() {
|
|
const checkboxes = document.querySelectorAll('.file-checkbox');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = this.checked;
|
|
});
|
|
updateBulkButtons();
|
|
});
|
|
|
|
// Add event listeners to individual checkboxes
|
|
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
|
checkbox.addEventListener('change', updateBulkButtons);
|
|
});
|
|
|
|
// Update current page
|
|
currentPage = page;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
pdfListContainer.innerHTML = `<div class="alert alert-danger">Erreur lors du chargement de la liste: ${error.message}</div>`;
|
|
} finally {
|
|
// Reset button state
|
|
refreshText.textContent = 'Actualiser';
|
|
refreshSpinner.classList.add('d-none');
|
|
refreshBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Event listener for refresh button
|
|
document.getElementById('refreshListBtn').addEventListener('click', function() {
|
|
loadPdfList(1); // Reset to first page on refresh
|
|
});
|
|
|
|
// Function to update bulk action buttons state
|
|
function updateBulkButtons() {
|
|
const selectedCount = document.querySelectorAll('.file-checkbox:checked').length;
|
|
document.getElementById('downloadSelectedBtn').disabled = selectedCount === 0;
|
|
document.getElementById('deleteSelectedBtn').disabled = selectedCount === 0;
|
|
document.getElementById('clearSelectionBtn').disabled = selectedCount === 0;
|
|
|
|
// Update select all header checkbox
|
|
const allChecked = selectedCount === document.querySelectorAll('.file-checkbox').length && selectedCount > 0;
|
|
document.getElementById('selectAllHeader').checked = allChecked;
|
|
}
|
|
|
|
// Load PDF list on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadPdfList(currentPage);
|
|
});
|
|
|
|
// Event listener for select all button
|
|
document.getElementById('selectAllBtn').addEventListener('click', function() {
|
|
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
|
checkbox.checked = true;
|
|
});
|
|
updateBulkButtons();
|
|
});
|
|
|
|
// Event listener for clear selection button
|
|
document.getElementById('clearSelectionBtn').addEventListener('click', function() {
|
|
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
|
checkbox.checked = false;
|
|
});
|
|
updateBulkButtons();
|
|
});
|
|
|
|
// Event listener for download selected button
|
|
document.getElementById('downloadSelectedBtn').addEventListener('click', async function() {
|
|
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
|
.map(checkbox => checkbox.getAttribute('data-filename'));
|
|
|
|
if (selectedFiles.length === 0) {
|
|
alert('Veuillez sélectionner au moins un fichier à télécharger.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create a form to submit the request (needed for file download)
|
|
const formData = new FormData();
|
|
formData.append('filenames', JSON.stringify(selectedFiles));
|
|
|
|
// Create a hidden form and submit it
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/bulk-download';
|
|
form.style.display = 'none';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'filenames';
|
|
input.value = JSON.stringify(selectedFiles);
|
|
form.appendChild(input);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
document.body.removeChild(form);
|
|
} catch (error) {
|
|
alert(`Erreur lors du téléchargement: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// Event listener for delete selected button
|
|
document.getElementById('deleteSelectedBtn').addEventListener('click', async function() {
|
|
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
|
.map(checkbox => checkbox.getAttribute('data-filename'));
|
|
|
|
if (selectedFiles.length === 0) {
|
|
alert('Veuillez sélectionner au moins un fichier à supprimer.');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Êtes-vous sûr de vouloir supprimer ${selectedFiles.length} fichier(s) ?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append('filenames', JSON.stringify(selectedFiles));
|
|
|
|
const response = await fetch('/bulk-delete', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
alert(`Erreur: ${result.error}`);
|
|
} else {
|
|
// Refresh the list (stay on current page)
|
|
loadPdfList(currentPage);
|
|
let message = result.message;
|
|
if (result.errors && result.errors.length > 0) {
|
|
message += `\nErreurs pour les fichiers: ${result.errors.join(', ')}`;
|
|
}
|
|
alert(message);
|
|
}
|
|
} catch (error) {
|
|
alert(`Erreur lors de la suppression: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
document.getElementById('exerciseForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const minTable = parseInt(document.getElementById('minTable').value);
|
|
const maxTable = parseInt(document.getElementById('maxTable').value);
|
|
const numExercises = parseInt(document.getElementById('numExercises').value);
|
|
|
|
const generateBtn = document.getElementById('generateBtn');
|
|
const buttonText = document.getElementById('buttonText');
|
|
const spinner = document.getElementById('spinner');
|
|
|
|
// Show loading state
|
|
buttonText.textContent = 'Génération en cours...';
|
|
spinner.classList.remove('d-none');
|
|
generateBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/generate', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
min_table: minTable,
|
|
max_table: maxTable,
|
|
num_exercises: numExercises
|
|
})
|
|
});
|
|
|
|
// Check if it's a redirect response (for automatic download)
|
|
if (response.redirected) {
|
|
// Automatically trigger download
|
|
window.location.href = response.url;
|
|
|
|
// Refresh the PDF list (reset to first page)
|
|
setTimeout(() => loadPdfList(1), 1000);
|
|
|
|
// Show success message
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const resultMessage = document.getElementById('resultMessage');
|
|
resultMessage.innerHTML = `
|
|
<div class="alert alert-success">
|
|
<h6>PDF généré avec succès!</h6>
|
|
<p>Le téléchargement devrait commencer automatiquement.</p>
|
|
<ul class="mb-0">
|
|
<li>Tables de ${minTable} à ${maxTable}</li>
|
|
<li>${numExercises} exercices générés</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
resultContainer.classList.remove('d-none');
|
|
} else {
|
|
// Handle JSON response (error case)
|
|
const result = await response.json();
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const resultMessage = document.getElementById('resultMessage');
|
|
|
|
if (result.error) {
|
|
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
|
}
|
|
|
|
resultContainer.classList.remove('d-none');
|
|
}
|
|
} catch (error) {
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const resultMessage = document.getElementById('resultMessage');
|
|
|
|
resultMessage.innerHTML = `<div class="alert alert-danger">Erreur: ${error.message}</div>`;
|
|
resultContainer.classList.remove('d-none');
|
|
} finally {
|
|
// Reset button state
|
|
buttonText.textContent = 'Générer le PDF';
|
|
spinner.classList.add('d-none');
|
|
generateBtn.disabled = false;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |