diff --git a/static/css/design-system.css b/static/css/design-system.css new file mode 100644 index 0000000..c77cb8e --- /dev/null +++ b/static/css/design-system.css @@ -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 */ +} \ No newline at end of file diff --git a/static/js/notytex.js b/static/js/notytex.js new file mode 100644 index 0000000..996a101 --- /dev/null +++ b/static/js/notytex.js @@ -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; \ No newline at end of file diff --git a/templates/assessments.html b/templates/assessments.html index 87bb238..9aec753 100644 --- a/templates/assessments.html +++ b/templates/assessments.html @@ -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 %}
- -
-
-
-

Mes Évaluations 📚

-

Gérez et organisez toutes vos évaluations

-
- - - - - - {{ assessments|length }} évaluations - - - - - - Année scolaire 2024-2025 - -
-
- -
-
+ {# Hero Section avec composant réutilisable #} + {% set meta_info = [ + { + 'icon': '', + 'text': assessments|length ~ ' évaluations' + }, + { + 'icon': '', + 'text': 'Année scolaire 2024-2025' + } + ] %} + {% set primary_action = { + 'url': url_for('assessments.new'), + 'text': 'Nouvelle évaluation', + 'icon': '' + } %} + {{ 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" + ) }} - -
-
-
-
- - -
-
- - -
-
- - -
- - -
- {{ assessments|length }} évaluation(s) -
+ {# 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}) %} +
+
+ {{ assessments|length }} évaluation(s)
-
+ {% endcall %} {% if assessments %} - + {% else %} @@ -260,39 +121,6 @@ {% endif %}
- +{# JavaScript géré par le système centralisé #} {% endblock %} diff --git a/templates/base.html b/templates/base.html index ff55181..7de0d62 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,17 +5,51 @@ {% block title %}Gestion Scolaire{% endblock %} + + + + + {% block scripts %}{% endblock %} \ No newline at end of file diff --git a/templates/components/assessment/assessment_card.html b/templates/components/assessment/assessment_card.html new file mode 100644 index 0000000..3515d4a --- /dev/null +++ b/templates/components/assessment/assessment_card.html @@ -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] %} + +
+ +
+
+
+
+ + + + +
+
Trimestre {{ assessment.trimester }}
+
+
{{ assessment.date.strftime('%d/%m/%Y') }}
+
+
+ + +
+

{{ assessment.title }}

+ + +
+
+ + + + {{ assessment.class_group.name }} +
+ +
+
+ + + + {{ assessment.exercises|length }} exercice(s) +
+ +
+ Coeff. {{ assessment.coefficient }} +
+
+ + + {% set progress = assessment.grading_progress %} +
+ {{ progress_indicator(progress, True, assessment.id) }} + + +
+ {{ progress.completed }}/{{ progress.total }} +
+
+
+ + {% if assessment.description %} +

{{ assessment.description }}

+ {% endif %} + + + +
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/components/common/macros.html b/templates/components/common/macros.html new file mode 100644 index 0000000..0551e63 --- /dev/null +++ b/templates/components/common/macros.html @@ -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") %} +
+
+
+ {% if caller %} + {{ caller() }} + {% endif %} +

{{ title }}

+ {% if subtitle %} +

{{ subtitle }}

+ {% endif %} + {% if meta_info %} +
+ {% for info in meta_info %} + + {% if info.icon %} + {{ info.icon|safe }} + {% endif %} + {{ info.text }} + + {% endfor %} +
+ {% endif %} +
+ +
+
+{% endmacro %} + +{# Macro pour card standardisée #} +{% macro card(title=None, actions=[], classes="", body_classes="p-6") %} +
+ {% if title %} +
+

{{ title }}

+ {% if actions %} +
+ {% for action in actions %} + + {{ action.text }} + + {% endfor %} +
+ {% endif %} +
+ {% endif %} +
+ {{ caller() }} +
+
+{% 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' +} %} + + {% if icon %} +
+ {{ icon|safe }} +
+ {% endif %} + {{ text }} +
+{% 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 %} + +{% else %} + +{% 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' %} +
+ + + + Correction 100% +
+{% elif progress.status == 'in_progress' %} + {% if clickable and assessment_id %} + +
+ + + + +
+ Correction {{ progress.percentage }}% + {% if clickable and assessment_id %}
{% else %}
{% endif %} +{% elif progress.status == 'not_started' %} + {% if clickable and assessment_id %} + + + + + Correction 0% + {% if clickable and assessment_id %}{% else %}
{% endif %} +{% else %} +
+ + + + Non définie +
+{% endif %} +{% endmacro %} + +{# Macro pour carte de statistique cliquable #} +{% macro stat_card(title, value, url, icon, color="blue", description=None) %} + +
+
+
{{ title }}
+
{{ value }}
+ {% if description %} +
+ + + + {{ description }} +
+ {% endif %} +
+
+ {{ icon|safe }} +
+
+
+{% endmacro %} + +{# Macro pour filtres standardisés #} +{% macro filter_section(filters, current_values={}) %} +
+
+
+ {% for filter in filters %} +
+ + +
+ {% endfor %} +
+ + + {% if caller %} +
+ {{ caller() }} +
+ {% endif %} +
+
+{% endmacro %} \ No newline at end of file