refactor: html organisation

This commit is contained in:
2025-08-05 10:56:28 +02:00
parent 1dccf28d5f
commit 4a2d8a73e1
6 changed files with 1209 additions and 240 deletions

View 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
View 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;