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;
|
||||
Reference in New Issue
Block a user