refactor: html organisation
This commit is contained in:
400
static/css/design-system.css
Normal file
400
static/css/design-system.css
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* NOTYTEX DESIGN SYSTEM
|
||||
* Système de design unifié pour l'application
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
DESIGN TOKENS
|
||||
======================================== */
|
||||
|
||||
:root {
|
||||
/* Couleurs primaires */
|
||||
--color-primary-50: #eff6ff;
|
||||
--color-primary-100: #dbeafe;
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-primary-600: #2563eb;
|
||||
--color-primary-700: #1d4ed8;
|
||||
--color-primary-800: #1e40af;
|
||||
|
||||
/* Couleurs fonctionnelles */
|
||||
--color-success-50: #ecfdf5;
|
||||
--color-success-100: #d1fae5;
|
||||
--color-success-500: #10b981;
|
||||
--color-success-600: #059669;
|
||||
--color-success-700: #047857;
|
||||
|
||||
--color-warning-50: #fffbeb;
|
||||
--color-warning-100: #fef3c7;
|
||||
--color-warning-500: #f59e0b;
|
||||
--color-warning-600: #d97706;
|
||||
--color-warning-700: #b45309;
|
||||
|
||||
--color-danger-50: #fef2f2;
|
||||
--color-danger-100: #fee2e2;
|
||||
--color-danger-500: #ef4444;
|
||||
--color-danger-600: #dc2626;
|
||||
--color-danger-700: #b91c1c;
|
||||
|
||||
/* Couleurs neutres */
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* Gradients standardisés */
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--gradient-success: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--gradient-warning: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||
--gradient-purple-blue: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-green: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--gradient-blue: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
|
||||
/* Espacements */
|
||||
--space-xs: 0.25rem; /* 4px */
|
||||
--space-sm: 0.5rem; /* 8px */
|
||||
--space-md: 1rem; /* 16px */
|
||||
--space-lg: 1.5rem; /* 24px */
|
||||
--space-xl: 2rem; /* 32px */
|
||||
--space-2xl: 3rem; /* 48px */
|
||||
|
||||
/* Tailles de police */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 1.875rem; /* 30px */
|
||||
--text-4xl: 2.25rem; /* 36px */
|
||||
|
||||
/* Rayons de bordure */
|
||||
--radius-sm: 0.125rem; /* 2px */
|
||||
--radius-md: 0.375rem; /* 6px */
|
||||
--radius-lg: 0.5rem; /* 8px */
|
||||
--radius-xl: 0.75rem; /* 12px */
|
||||
--radius-2xl: 1rem; /* 16px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Ombres */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
CLASSES UTILITAIRES PERSONNALISÉES
|
||||
======================================== */
|
||||
|
||||
/* Focus visible amélioré */
|
||||
.focus-ring {
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
/* Gradients réutilisables */
|
||||
.bg-gradient-primary {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.bg-gradient-success {
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.bg-gradient-purple-blue {
|
||||
background: var(--gradient-purple-blue);
|
||||
}
|
||||
|
||||
/* Animation hover pour les cartes */
|
||||
.card-hover {
|
||||
@apply transition-all duration-300 hover:shadow-xl hover:scale-105;
|
||||
}
|
||||
|
||||
/* Boutons avec styles cohérents */
|
||||
.btn-base {
|
||||
@apply inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-300 focus-ring;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply btn-base px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
@apply btn-base px-4 py-2 text-base;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply btn-base px-6 py-3 text-lg;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn-base bg-blue-600 text-white hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
@apply btn-base bg-green-600 text-white hover:bg-green-700;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@apply btn-base bg-orange-600 text-white hover:bg-orange-700;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply btn-base bg-red-600 text-white hover:bg-red-700;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply btn-base bg-transparent border-2 border-gray-300 text-gray-700 hover:bg-gray-50;
|
||||
}
|
||||
|
||||
/* États de progression standardisés */
|
||||
.progress-not-started {
|
||||
@apply bg-red-100 text-red-800 border border-red-200;
|
||||
}
|
||||
|
||||
.progress-in-progress {
|
||||
@apply bg-orange-100 text-orange-800 border border-orange-200;
|
||||
}
|
||||
|
||||
.progress-completed {
|
||||
@apply bg-green-100 text-green-800 border border-green-200;
|
||||
}
|
||||
|
||||
/* Indicateurs visuels */
|
||||
.indicator-badge {
|
||||
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
COMPOSANTS SPÉCIALISÉS
|
||||
======================================== */
|
||||
|
||||
/* Hero sections */
|
||||
.hero-section {
|
||||
@apply bg-gradient-to-r text-white rounded-xl p-8 shadow-lg;
|
||||
}
|
||||
|
||||
/* Cards uniformes */
|
||||
.card {
|
||||
@apply bg-white rounded-xl shadow-lg overflow-hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@apply px-6 py-4 border-b border-gray-200 flex items-center justify-between;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@apply px-6 py-4 border-t border-gray-200 bg-gray-50;
|
||||
}
|
||||
|
||||
/* Grilles de statistiques */
|
||||
.stats-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@apply block bg-white rounded-lg shadow hover:shadow-lg transition-all duration-300 p-6 transform hover:scale-105 group;
|
||||
}
|
||||
|
||||
/* Filtres */
|
||||
.filter-section {
|
||||
@apply bg-white rounded-lg shadow p-6;
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
@apply border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent;
|
||||
}
|
||||
|
||||
/* Actions grids */
|
||||
.action-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
@apply group rounded-xl p-6 transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl text-white;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
ANIMATIONS ET EFFETS
|
||||
======================================== */
|
||||
|
||||
/* Animation d'apparition */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Animation des messages flash */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Effet de pulsation pour les éléments en attente */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Rotation pour les indicateurs de chargement */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin-animation {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
RESPONSIVE DESIGN
|
||||
======================================== */
|
||||
|
||||
/* Masquer/afficher selon la taille d'écran */
|
||||
@media (max-width: 768px) {
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.show-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Ajustements pour mobile */
|
||||
.hero-section {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
@apply grid-cols-1 gap-3;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@apply grid-cols-1 gap-4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.hide-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.show-desktop {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
ACCESSIBILITÉ
|
||||
======================================== */
|
||||
|
||||
/* Skip links */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 6px;
|
||||
background: var(--color-gray-900);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
text-decoration: none;
|
||||
z-index: 1000;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/* Contraste élevé */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--color-gray-600: #000000;
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply border-2 border-gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
/* Réduction de mouvement */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus visible pour la navigation au clavier */
|
||||
.focus-visible {
|
||||
outline: 2px solid var(--color-primary-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
DARK MODE (préparation future)
|
||||
======================================== */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-gray-50: #1f2937;
|
||||
--color-gray-100: #374151;
|
||||
--color-gray-900: #f9fafb;
|
||||
}
|
||||
|
||||
/* Note: Le dark mode sera implémenté dans une future phase */
|
||||
}
|
||||
387
static/js/notytex.js
Normal file
387
static/js/notytex.js
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* NOTYTEX - JavaScript Application Core
|
||||
* Fonctionnalités JavaScript centralisées et réutilisables
|
||||
*/
|
||||
|
||||
const Notytex = {
|
||||
|
||||
// Configuration globale
|
||||
config: {
|
||||
transitions: {
|
||||
fast: 150,
|
||||
normal: 300,
|
||||
slow: 500
|
||||
},
|
||||
breakpoints: {
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280
|
||||
}
|
||||
},
|
||||
|
||||
// Utilitaires généraux
|
||||
utils: {
|
||||
/**
|
||||
* Débounce une fonction
|
||||
*/
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Vérifie si un élément est visible dans le viewport
|
||||
*/
|
||||
isInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Anime l'apparition des éléments
|
||||
*/
|
||||
animateOnScroll() {
|
||||
const elements = document.querySelectorAll('[data-animate]');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const animationType = entry.target.dataset.animate;
|
||||
entry.target.classList.add(`animate-${animationType}`);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
});
|
||||
|
||||
elements.forEach(el => observer.observe(el));
|
||||
},
|
||||
|
||||
/**
|
||||
* Copie du texte dans le presse-papiers
|
||||
*/
|
||||
async copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.showToast('Copié dans le presse-papiers', 'success');
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la copie:', err);
|
||||
this.showToast('Erreur lors de la copie', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Affiche une notification toast
|
||||
*/
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
const toast = document.createElement('div');
|
||||
const typeClasses = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-orange-500 text-white',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${typeClasses[type]} transition-all duration-300 transform translate-x-full`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animation d'entrée
|
||||
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
|
||||
|
||||
// Suppression automatique
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => document.body.removeChild(toast), 300);
|
||||
}, duration);
|
||||
}
|
||||
},
|
||||
|
||||
// Gestion des filtres
|
||||
filters: {
|
||||
/**
|
||||
* Initialise les filtres pour une page
|
||||
*/
|
||||
init(config = {}) {
|
||||
const defaultConfig = {
|
||||
autoSubmit: true,
|
||||
debounceTime: 300,
|
||||
preserveState: true
|
||||
};
|
||||
|
||||
const settings = { ...defaultConfig, ...config };
|
||||
|
||||
const filterElements = document.querySelectorAll('[data-filter]');
|
||||
const applyFilters = Notytex.utils.debounce(this.applyFilters, settings.debounceTime);
|
||||
|
||||
filterElements.forEach(element => {
|
||||
element.addEventListener('change', applyFilters);
|
||||
});
|
||||
|
||||
// Restaurer l'état des filtres depuis l'URL
|
||||
if (settings.preserveState) {
|
||||
this.restoreFromURL();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Applique les filtres en construisant l'URL
|
||||
*/
|
||||
applyFilters() {
|
||||
const params = new URLSearchParams();
|
||||
const filterElements = document.querySelectorAll('[data-filter]');
|
||||
|
||||
filterElements.forEach(element => {
|
||||
const filterName = element.dataset.filter;
|
||||
const value = element.value;
|
||||
|
||||
if (value && value !== '') {
|
||||
params.set(filterName, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Mise à jour de l'URL
|
||||
const url = new URL(window.location);
|
||||
url.search = params.toString();
|
||||
|
||||
// Navigation sans rechargement complet si possible
|
||||
if (history.pushState) {
|
||||
history.pushState(null, '', url.toString());
|
||||
// Déclenchement d'un événement personnalisé pour les composants qui écoutent
|
||||
window.dispatchEvent(new CustomEvent('filtersChanged', { detail: params }));
|
||||
} else {
|
||||
// Fallback pour les anciens navigateurs
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restaure les filtres depuis l'URL
|
||||
*/
|
||||
restoreFromURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
params.forEach((value, key) => {
|
||||
const element = document.querySelector(`[data-filter="${key}"]`);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Gestion des modales
|
||||
modals: {
|
||||
/**
|
||||
* Ouvre une modale
|
||||
*/
|
||||
open(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
// Animation d'entrée
|
||||
setTimeout(() => {
|
||||
modal.querySelector('.modal-content')?.classList.add('scale-100');
|
||||
modal.querySelector('.modal-content')?.classList.remove('scale-95');
|
||||
}, 10);
|
||||
|
||||
// Focus sur la modale pour l'accessibilité
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
modal.focus();
|
||||
|
||||
// Écouter la touche Escape
|
||||
document.addEventListener('keydown', this.handleEscapeKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ferme une modale
|
||||
*/
|
||||
close(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
|
||||
// Animation de sortie
|
||||
modal.querySelector('.modal-content')?.classList.add('scale-95');
|
||||
modal.querySelector('.modal-content')?.classList.remove('scale-100');
|
||||
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
}, 150);
|
||||
|
||||
document.removeEventListener('keydown', this.handleEscapeKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gère la touche Escape pour fermer les modales
|
||||
*/
|
||||
handleEscapeKey(event) {
|
||||
if (event.key === 'Escape') {
|
||||
const openModal = document.querySelector('.modal:not(.hidden)');
|
||||
if (openModal) {
|
||||
Notytex.modals.close(openModal.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Gestion des données en temps réel
|
||||
realtime: {
|
||||
/**
|
||||
* Met à jour les indicateurs de progression
|
||||
*/
|
||||
updateProgressIndicators() {
|
||||
const indicators = document.querySelectorAll('[data-progress]');
|
||||
|
||||
indicators.forEach(indicator => {
|
||||
const assessmentId = indicator.dataset.assessmentId;
|
||||
|
||||
// Ici on peut faire un appel AJAX pour récupérer les données actualisées
|
||||
// Pour l'instant, on simule avec une animation
|
||||
this.animateProgress(indicator);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Animation des cercles de progression
|
||||
*/
|
||||
animateProgress(element) {
|
||||
const circle = element.querySelector('circle[stroke-dashoffset]');
|
||||
if (!circle) return;
|
||||
|
||||
const currentOffset = parseFloat(circle.getAttribute('stroke-dashoffset'));
|
||||
const circumference = 2 * Math.PI * parseFloat(circle.getAttribute('r'));
|
||||
|
||||
// Animation fluide
|
||||
circle.style.transition = 'stroke-dashoffset 1s ease-in-out';
|
||||
}
|
||||
},
|
||||
|
||||
// Gestion des confirmations
|
||||
confirmations: {
|
||||
/**
|
||||
* Affiche une confirmation avant une action
|
||||
*/
|
||||
show(message, callback, options = {}) {
|
||||
const defaultOptions = {
|
||||
title: 'Confirmation',
|
||||
confirmText: 'Confirmer',
|
||||
cancelText: 'Annuler',
|
||||
type: 'warning'
|
||||
};
|
||||
|
||||
const settings = { ...defaultOptions, ...options };
|
||||
|
||||
// Si on a SweetAlert2 disponible, on l'utilise
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
title: settings.title,
|
||||
text: message,
|
||||
icon: settings.type,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: settings.confirmText,
|
||||
cancelButtonText: settings.cancelText,
|
||||
confirmButtonColor: settings.type === 'danger' ? '#dc2626' : '#3b82f6'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback avec confirm() natif
|
||||
if (confirm(message)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Initialisation de l'application
|
||||
init() {
|
||||
console.log('🎓 Notytex Application Initialized');
|
||||
|
||||
// Initialisation des fonctionnalités de base
|
||||
this.utils.animateOnScroll();
|
||||
|
||||
// Initialisation des filtres si présents
|
||||
if (document.querySelector('[data-filter]')) {
|
||||
this.filters.init();
|
||||
}
|
||||
|
||||
// Gestionnaires d'événements globaux
|
||||
this.bindGlobalEvents();
|
||||
|
||||
// Mise à jour périodique des indicateurs (optionnel)
|
||||
if (document.querySelector('[data-progress]')) {
|
||||
setInterval(() => {
|
||||
this.realtime.updateProgressIndicators();
|
||||
}, 30000); // Toutes les 30 secondes
|
||||
}
|
||||
},
|
||||
|
||||
// Événements globaux
|
||||
bindGlobalEvents() {
|
||||
// Fermeture des dropdowns au clic extérieur
|
||||
document.addEventListener('click', (event) => {
|
||||
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
|
||||
dropdowns.forEach(dropdown => {
|
||||
if (!dropdown.contains(event.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Gestion des liens de confirmation
|
||||
document.addEventListener('click', (event) => {
|
||||
const confirmLink = event.target.closest('[data-confirm]');
|
||||
if (confirmLink) {
|
||||
event.preventDefault();
|
||||
const message = confirmLink.dataset.confirm;
|
||||
this.confirmations.show(message, () => {
|
||||
if (confirmLink.tagName === 'A') {
|
||||
window.location.href = confirmLink.href;
|
||||
} else if (confirmLink.tagName === 'BUTTON') {
|
||||
confirmLink.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion des boutons de copie
|
||||
document.addEventListener('click', (event) => {
|
||||
const copyButton = event.target.closest('[data-copy]');
|
||||
if (copyButton) {
|
||||
const textToCopy = copyButton.dataset.copy;
|
||||
this.utils.copyToClipboard(textToCopy);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialisation au chargement du DOM
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Notytex.init();
|
||||
});
|
||||
|
||||
// Export pour utilisation dans d'autres scripts
|
||||
window.Notytex = Notytex;
|
||||
@@ -1,81 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
{% from 'components/common/macros.html' import hero_section, filter_section %}
|
||||
{% from 'components/assessment/assessment_card.html' import assessment_card %}
|
||||
|
||||
{% block title %}Évaluations - Gestion Scolaire{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold mb-2">Mes Évaluations 📚</h1>
|
||||
<p class="text-xl opacity-90 mb-1">Gérez et organisez toutes vos évaluations</p>
|
||||
<div class="flex items-center space-x-6 text-sm opacity-75 mt-3">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
|
||||
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ assessments|length }} évaluations
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Année scolaire 2024-2025
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<a href="{{ url_for('assessments.new') }}"
|
||||
class="bg-white/20 hover:bg-white/30 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Nouvelle évaluation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Hero Section avec composant réutilisable #}
|
||||
{% set meta_info = [
|
||||
{
|
||||
'icon': '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/></svg>',
|
||||
'text': assessments|length ~ ' évaluations'
|
||||
},
|
||||
{
|
||||
'icon': '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>',
|
||||
'text': 'Année scolaire 2024-2025'
|
||||
}
|
||||
] %}
|
||||
{% set primary_action = {
|
||||
'url': url_for('assessments.new'),
|
||||
'text': 'Nouvelle évaluation',
|
||||
'icon': '<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>'
|
||||
} %}
|
||||
{{ hero_section(
|
||||
title="Mes Évaluations 📚",
|
||||
subtitle="Gérez et organisez toutes vos évaluations",
|
||||
meta_info=meta_info,
|
||||
primary_action=primary_action,
|
||||
gradient_class="from-purple-600 to-blue-600"
|
||||
) }}
|
||||
|
||||
<!-- Filtres et actions -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
||||
<div class="flex flex-col md:flex-row md:items-center space-y-3 md:space-y-0 md:space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm font-medium text-gray-700">Trimestre :</label>
|
||||
<select id="trimester-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
<option value="">Tous</option>
|
||||
<option value="1" {% if current_trimester == '1' %}selected{% endif %}>Trimestre 1</option>
|
||||
<option value="2" {% if current_trimester == '2' %}selected{% endif %}>Trimestre 2</option>
|
||||
<option value="3" {% if current_trimester == '3' %}selected{% endif %}>Trimestre 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm font-medium text-gray-700">Classe :</label>
|
||||
<select id="class-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
<option value="">Toutes</option>
|
||||
{% for class_group in classes %}
|
||||
<option value="{{ class_group.id }}" {% if current_class == class_group.id|string %}selected{% endif %}>
|
||||
{{ class_group.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm font-medium text-gray-700">Tri :</label>
|
||||
<select id="sort-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
<option value="date_desc" {% if current_sort == 'date_desc' %}selected{% endif %}>Plus récent</option>
|
||||
<option value="date_asc" {% if current_sort == 'date_asc' %}selected{% endif %}>Plus ancien</option>
|
||||
<option value="title" {% if current_sort == 'title' %}selected{% endif %}>Titre A-Z</option>
|
||||
<option value="class" {% if current_sort == 'class' %}selected{% endif %}>Classe</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Compteur de résultats -->
|
||||
<div class="text-sm text-gray-500 font-medium">
|
||||
{{ assessments|length }} évaluation(s)
|
||||
</div>
|
||||
{# Filtres avec composant réutilisable #}
|
||||
{% set class_options = [{'value': '', 'label': 'Toutes'}] %}
|
||||
{% for class_group in classes %}
|
||||
{% set _ = class_options.append({'value': class_group.id|string, 'label': class_group.name}) %}
|
||||
{% endfor %}
|
||||
|
||||
{% set filters = [
|
||||
{
|
||||
'id': 'trimester-filter',
|
||||
'label': 'Trimestre',
|
||||
'options': [
|
||||
{'value': '', 'label': 'Tous'},
|
||||
{'value': '1', 'label': 'Trimestre 1'},
|
||||
{'value': '2', 'label': 'Trimestre 2'},
|
||||
{'value': '3', 'label': 'Trimestre 3'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': 'class-filter',
|
||||
'label': 'Classe',
|
||||
'options': class_options
|
||||
},
|
||||
{
|
||||
'id': 'sort-filter',
|
||||
'label': 'Tri',
|
||||
'options': [
|
||||
{'value': 'date_desc', 'label': 'Plus récent'},
|
||||
{'value': 'date_asc', 'label': 'Plus ancien'},
|
||||
{'value': 'title', 'label': 'Titre A-Z'},
|
||||
{'value': 'class', 'label': 'Classe'}
|
||||
]
|
||||
}
|
||||
] %}
|
||||
|
||||
{% call filter_section(filters, {'trimester-filter': current_trimester, 'class-filter': current_class, 'sort-filter': current_sort}) %}
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-sm text-gray-500 font-medium">
|
||||
{{ assessments|length }} évaluation(s)
|
||||
</div>
|
||||
<div class="md:hidden">
|
||||
<a href="{{ url_for('assessments.new') }}"
|
||||
@@ -87,144 +79,13 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% if assessments %}
|
||||
<!-- Grille d'évaluations modernisée -->
|
||||
<!-- Grille d'évaluations avec composants -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for assessment in assessments %}
|
||||
{% set trimester_colors = {
|
||||
1: {'bg': 'from-blue-500 to-blue-600', 'accent': 'blue', 'icon_bg': 'bg-blue-100', 'icon_text': 'text-blue-600'},
|
||||
2: {'bg': 'from-green-500 to-green-600', 'accent': 'green', 'icon_bg': 'bg-green-100', 'icon_text': 'text-green-600'},
|
||||
3: {'bg': 'from-orange-500 to-orange-600', 'accent': 'orange', 'icon_bg': 'bg-orange-100', 'icon_text': 'text-orange-600'}
|
||||
} %}
|
||||
{% set colors = trimester_colors[assessment.trimester] %}
|
||||
|
||||
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 overflow-hidden">
|
||||
<!-- Header avec gradient thématique -->
|
||||
<div class="bg-gradient-to-r {{ colors.bg }} p-4 text-white">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
|
||||
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-sm font-semibold">Trimestre {{ assessment.trimester }}</div>
|
||||
</div>
|
||||
<div class="text-xs opacity-75">{{ assessment.date.strftime('%d/%m/%Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu principal -->
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-2 line-clamp-2">{{ assessment.title }}</h3>
|
||||
|
||||
<!-- Informations clés -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="font-medium">{{ assessment.class_group.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
|
||||
</svg>
|
||||
<span>{{ assessment.exercises|length }} exercice(s)</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-{{ colors.accent }}-100 text-{{ colors.accent }}-800 text-xs px-2 py-1 rounded-full font-medium">
|
||||
Coeff. {{ assessment.coefficient }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicateur de progression des notes -->
|
||||
{% set progress = assessment.grading_progress %}
|
||||
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<!-- Indicateur fusionné cliquable -->
|
||||
{% if progress.status == 'completed' %}
|
||||
<div class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-green-100 text-green-800 border border-green-200">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Correction 100% </span>
|
||||
</div>
|
||||
{% elif progress.status == 'in_progress' %}
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
|
||||
class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-orange-100 text-orange-800 border border-orange-200 hover:bg-orange-200 transition-colors cursor-pointer">
|
||||
<div class="relative w-4 h-4 mr-2">
|
||||
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" class="text-orange-300"/>
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none"
|
||||
class="text-orange-600" stroke-dasharray="37.7"
|
||||
stroke-dashoffset="{{ 37.7 - (37.7 * progress.percentage / 100) }}"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="font-semibold">Correction {{ progress.percentage }}% </span>
|
||||
</a>
|
||||
{% elif progress.status == 'not_started' %}
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
|
||||
class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-red-100 text-red-800 border border-red-200 hover:bg-red-200 transition-colors cursor-pointer">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Correction 0%</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-gray-100 text-gray-800 border border-gray-200">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Non définie</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Info détaillée -->
|
||||
<div class="text-xs text-gray-500 text-right">
|
||||
{{ progress.completed }}/{{ progress.total }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if assessment.description %}
|
||||
<p class="text-sm text-gray-600 mb-4 line-clamp-2">{{ assessment.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('assessments.detail', id=assessment.id) }}"
|
||||
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Détails
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
|
||||
class="flex-1 bg-{{ colors.accent }}-50 hover:bg-{{ colors.accent }}-100 text-{{ colors.accent }}-700 hover:text-{{ colors.accent }}-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
Noter
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('assessments.edit', id=assessment.id) }}"
|
||||
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Modifier
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ assessment_card(assessment) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -260,39 +121,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Gestion des filtres
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const trimesterFilter = document.getElementById('trimester-filter');
|
||||
const classFilter = document.getElementById('class-filter');
|
||||
const sortFilter = document.getElementById('sort-filter');
|
||||
|
||||
function applyFilters() {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (trimesterFilter.value) {
|
||||
params.set('trimester', trimesterFilter.value);
|
||||
}
|
||||
|
||||
if (classFilter.value) {
|
||||
params.set('class', classFilter.value);
|
||||
}
|
||||
|
||||
if (sortFilter.value) {
|
||||
params.set('sort', sortFilter.value);
|
||||
}
|
||||
|
||||
// Rediriger avec les nouveaux paramètres
|
||||
const url = new URL(window.location);
|
||||
url.search = params.toString();
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
// Écouter les changements sur tous les filtres
|
||||
[trimesterFilter, classFilter, sortFilter].forEach(filter => {
|
||||
filter.addEventListener('change', applyFilters);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{# JavaScript géré par le système centralisé #}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,17 +5,51 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Gestion Scolaire{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/design-system.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
<script>
|
||||
// Configuration Tailwind pour dark mode et focus
|
||||
// Configuration Tailwind étendue avec design tokens
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
},
|
||||
success: {
|
||||
50: '#ecfdf5',
|
||||
100: '#d1fae5',
|
||||
500: '#10b981',
|
||||
600: '#059669',
|
||||
700: '#047857',
|
||||
},
|
||||
warning: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in-up': 'fadeInUp 0.5s ease-out',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,5 +116,11 @@
|
||||
<p>© 2025 Gestion Scolaire - Application de gestion des évaluations</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- JavaScript Core -->
|
||||
<script src="{{ url_for('static', filename='js/notytex.js') }}"></script>
|
||||
|
||||
<!-- JavaScript spécifique aux pages -->
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
101
templates/components/assessment/assessment_card.html
Normal file
101
templates/components/assessment/assessment_card.html
Normal file
@@ -0,0 +1,101 @@
|
||||
{# Composant pour carte d'évaluation dans la liste #}
|
||||
{% from 'components/common/macros.html' import progress_indicator %}
|
||||
|
||||
{% macro assessment_card(assessment) %}
|
||||
{% set trimester_colors = {
|
||||
1: {'bg': 'from-blue-500 to-blue-600', 'accent': 'blue', 'icon_bg': 'bg-blue-100', 'icon_text': 'text-blue-600'},
|
||||
2: {'bg': 'from-green-500 to-green-600', 'accent': 'green', 'icon_bg': 'bg-green-100', 'icon_text': 'text-green-600'},
|
||||
3: {'bg': 'from-orange-500 to-orange-600', 'accent': 'orange', 'icon_bg': 'bg-orange-100', 'icon_text': 'text-orange-600'}
|
||||
} %}
|
||||
{% set colors = trimester_colors[assessment.trimester] %}
|
||||
|
||||
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 overflow-hidden">
|
||||
<!-- Header avec gradient thématique -->
|
||||
<div class="bg-gradient-to-r {{ colors.bg }} p-4 text-white">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
|
||||
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-sm font-semibold">Trimestre {{ assessment.trimester }}</div>
|
||||
</div>
|
||||
<div class="text-xs opacity-75">{{ assessment.date.strftime('%d/%m/%Y') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu principal -->
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-2 line-clamp-2">{{ assessment.title }}</h3>
|
||||
|
||||
<!-- Informations clés -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="font-medium">{{ assessment.class_group.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
|
||||
</svg>
|
||||
<span>{{ assessment.exercises|length }} exercice(s)</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-{{ colors.accent }}-100 text-{{ colors.accent }}-800 text-xs px-2 py-1 rounded-full font-medium">
|
||||
Coeff. {{ assessment.coefficient }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicateur de progression des notes -->
|
||||
{% set progress = assessment.grading_progress %}
|
||||
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
{{ progress_indicator(progress, True, assessment.id) }}
|
||||
|
||||
<!-- Info détaillée -->
|
||||
<div class="text-xs text-gray-500 text-right">
|
||||
{{ progress.completed }}/{{ progress.total }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if assessment.description %}
|
||||
<p class="text-sm text-gray-600 mb-4 line-clamp-2">{{ assessment.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('assessments.detail', id=assessment.id) }}"
|
||||
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Détails
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
|
||||
class="flex-1 bg-{{ colors.accent }}-50 hover:bg-{{ colors.accent }}-100 text-{{ colors.accent }}-700 hover:text-{{ colors.accent }}-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
Noter
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('assessments.edit', id=assessment.id) }}"
|
||||
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Modifier
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
213
templates/components/common/macros.html
Normal file
213
templates/components/common/macros.html
Normal file
@@ -0,0 +1,213 @@
|
||||
{# Macros pour composants réutilisables #}
|
||||
|
||||
{# Macro pour section hero avec gradient #}
|
||||
{% macro hero_section(title, subtitle=None, meta_info=[], primary_action=None, icon=None, gradient_class="from-purple-600 to-blue-600") %}
|
||||
<div class="bg-gradient-to-r {{ gradient_class }} text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
<h1 class="text-4xl font-bold mb-2">{{ title }}</h1>
|
||||
{% if subtitle %}
|
||||
<p class="text-xl opacity-90 mb-1">{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
{% if meta_info %}
|
||||
<div class="flex items-center space-x-6 text-sm opacity-75 mt-3">
|
||||
{% for info in meta_info %}
|
||||
<span class="flex items-center">
|
||||
{% if info.icon %}
|
||||
{{ info.icon|safe }}
|
||||
{% endif %}
|
||||
{{ info.text }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
{% if primary_action %}
|
||||
<a href="{{ primary_action.url }}"
|
||||
class="bg-white/20 hover:bg-white/30 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 flex items-center">
|
||||
{% if primary_action.icon %}
|
||||
{{ primary_action.icon|safe }}
|
||||
{% endif %}
|
||||
{{ primary_action.text }}
|
||||
</a>
|
||||
{% elif icon %}
|
||||
<div class="w-24 h-24 bg-white/20 rounded-full flex items-center justify-center">
|
||||
{{ icon|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour card standardisée #}
|
||||
{% macro card(title=None, actions=[], classes="", body_classes="p-6") %}
|
||||
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 {{ classes }}">
|
||||
{% if title %}
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">{{ title }}</h2>
|
||||
{% if actions %}
|
||||
<div class="flex space-x-2">
|
||||
{% for action in actions %}
|
||||
<a href="{{ action.url }}" class="{{ action.classes|default('text-blue-600 hover:text-blue-800 text-sm font-medium') }}">
|
||||
{{ action.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="{{ body_classes }}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour bouton avec gradient #}
|
||||
{% macro gradient_button(url, text, icon=None, gradient_class="from-blue-500 to-blue-600", hover_gradient="from-blue-600 to-blue-700", size="md", classes="") %}
|
||||
{% set size_classes = {
|
||||
'sm': 'px-4 py-2 text-sm',
|
||||
'md': 'px-6 py-3 text-base',
|
||||
'lg': 'px-8 py-4 text-lg'
|
||||
} %}
|
||||
<a href="{{ url }}"
|
||||
class="group bg-gradient-to-r {{ gradient_class }} hover:{{ hover_gradient }} text-white rounded-xl {{ size_classes[size] }} transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl font-semibold flex items-center {{ classes }}">
|
||||
{% if icon %}
|
||||
<div class="w-5 h-5 mr-2 group-hover:scale-110 transition-transform">
|
||||
{{ icon|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ text }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour action card (comme les cartes d'actions principales) #}
|
||||
{% macro action_card(url, title, subtitle, icon, gradient_class="from-green-500 to-green-600", hover_gradient="from-green-600 to-green-700", is_button=False) %}
|
||||
{% if is_button %}
|
||||
<button onclick="{{ url }}"
|
||||
{% else %}
|
||||
<a href="{{ url }}"
|
||||
{% endif %}
|
||||
class="group bg-gradient-to-r {{ gradient_class }} hover:{{ hover_gradient }} text-white rounded-xl p-6 transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
|
||||
{{ icon|safe }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-1">{{ title }}</h3>
|
||||
<p class="text-sm opacity-90">{{ subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if is_button %}
|
||||
</button>
|
||||
{% else %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour indicateur de progression #}
|
||||
{% macro progress_indicator(progress, clickable=True, assessment_id=None, compact=False) %}
|
||||
{% set base_classes = "inline-flex items-center rounded-full text-xs font-medium border" %}
|
||||
{% set size_classes = "px-3 py-2" if not compact else "px-2 py-1" %}
|
||||
|
||||
{% if progress.status == 'completed' %}
|
||||
<div class="{{ base_classes }} {{ size_classes }} bg-green-100 text-green-800 border-green-200">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Correction 100%</span>
|
||||
</div>
|
||||
{% elif progress.status == 'in_progress' %}
|
||||
{% if clickable and assessment_id %}
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment_id) }}"
|
||||
{% else %}
|
||||
<div
|
||||
{% endif %}
|
||||
class="{{ base_classes }} {{ size_classes }} bg-orange-100 text-orange-800 border-orange-200 {% if clickable %}hover:bg-orange-200 transition-colors cursor-pointer{% endif %}">
|
||||
<div class="relative w-4 h-4 mr-2">
|
||||
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" class="text-orange-300"/>
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none"
|
||||
class="text-orange-600" stroke-dasharray="37.7"
|
||||
stroke-dashoffset="{{ 37.7 - (37.7 * progress.percentage / 100) }}"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="font-semibold">Correction {{ progress.percentage }}%</span>
|
||||
{% if clickable and assessment_id %}</a>{% else %}</div>{% endif %}
|
||||
{% elif progress.status == 'not_started' %}
|
||||
{% if clickable and assessment_id %}
|
||||
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment_id) }}"
|
||||
{% else %}
|
||||
<div
|
||||
{% endif %}
|
||||
class="{{ base_classes }} {{ size_classes }} bg-red-100 text-red-800 border-red-200 {% if clickable %}hover:bg-red-200 transition-colors cursor-pointer{% endif %}">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Correction 0%</span>
|
||||
{% if clickable and assessment_id %}</a>{% else %}</div>{% endif %}
|
||||
{% else %}
|
||||
<div class="{{ base_classes }} {{ size_classes }} bg-gray-100 text-gray-800 border-gray-200">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="font-semibold">Non définie</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour carte de statistique cliquable #}
|
||||
{% macro stat_card(title, value, url, icon, color="blue", description=None) %}
|
||||
<a href="{{ url }}" class="block bg-white rounded-lg shadow hover:shadow-lg transition-all duration-300 p-6 transform hover:scale-105 group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-500 group-hover:text-{{ color }}-600 transition-colors">{{ title }}</div>
|
||||
<div class="text-3xl font-bold text-gray-900 group-hover:text-{{ color }}-700 transition-colors">{{ value }}</div>
|
||||
{% if description %}
|
||||
<div class="text-xs text-{{ color }}-600 flex items-center mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-{{ color }}-100 group-hover:bg-{{ color }}-200 rounded-full flex items-center justify-center transition-colors">
|
||||
{{ icon|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro pour filtres standardisés #}
|
||||
{% macro filter_section(filters, current_values={}) %}
|
||||
<div class="filter-section">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
||||
<div class="flex flex-col md:flex-row md:items-center space-y-3 md:space-y-0 md:space-x-4">
|
||||
{% for filter in filters %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm font-medium text-gray-700">{{ filter.label }} :</label>
|
||||
<select data-filter="{{ filter.id.replace('-filter', '') }}" class="filter-control">
|
||||
{% for option in filter.options %}
|
||||
<option value="{{ option.value }}" {% if current_values.get(filter.id) == option.value %}selected{% endif %}>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Zone pour contenu additionnel -->
|
||||
{% if caller %}
|
||||
<div class="flex items-center space-x-4">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user