Bootstrap 5
Calendar
Widget
Sidebar
Evenements
Responsive
Widget calendrier interactif Bootstrap 5 avec sidebar navigation, gestion mois et événements colorés pour interface planification et réservation.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="copyright" content="AngularForAll" />
<meta name="author" content="AngularForAll" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="public, max-age=604800" />
<title>Snippets Calendar Widget Bootstrap5 2026 05060029 | AngularForAll</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--sidebar-width: 380px;
--primary-color: #4e73df;
--event-work: #4e73df;
--event-personal: #1cc88a;
--event-meeting: #f6c23e;
--event-important: #e74a3b;
--event-other: #36b9cc;
}
body {
background: #f8f9fc;
min-height: 100vh;
}
.app-container {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
background: white;
box-shadow: 2px 0 15px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
border-right: 1px solid #e3e6f0;
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 1000;
overflow-y: auto;
}
.sidebar-header {
background: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
color: white;
padding: 1.5rem;
text-align: center;
}
.sidebar-title {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.sidebar-subtitle {
font-size: 0.85rem;
opacity: 0.9;
}
/* Calendrier Widget */
.calendar-widget {
padding: 1rem;
flex: 1;
}
.calendar-nav-mini {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding: 0.5rem;
background: #f8f9fc;
border-radius: 10px;
}
.month-title-mini {
font-weight: 700;
font-size: 1rem;
color: #5a5c69;
text-transform: capitalize;
margin: 0;
}
.btn-nav-mini {
background: white;
border: 1px solid #e3e6f0;
color: #5a5c69;
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
}
.btn-nav-mini:hover {
background: #4e73df;
color: white;
border-color: #4e73df;
}
.weekdays-mini {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 5px;
}
.weekday-mini {
text-align: center;
font-size: 0.7rem;
font-weight: 700;
color: #4e73df;
padding: 0.3rem 0;
text-transform: uppercase;
}
.weekday-mini.weekend {
color: #e74a3b;
}
.days-grid-mini {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.day-cell-mini {
aspect-ratio: 1;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.8rem;
position: relative;
color: #5a5c69;
font-weight: 500;
min-height: 35px;
}
.day-cell-mini:hover {
background: #f0f2f5;
}
.day-cell-mini.other-month {
color: #d1d3e2;
}
.day-cell-mini.today {
background: #4e73df;
color: white;
font-weight: 700;
box-shadow: 0 3px 10px rgba(78, 115, 223, 0.3);
}
.day-cell-mini.selected {
background: rgba(78, 115, 223, 0.1);
color: #4e73df;
font-weight: 700;
box-shadow: 0 0 0 2px #4e73df;
}
.day-cell-mini.weekend:not(.today) {
color: #e74a3b;
}
.day-number-mini {
font-size: 0.75rem;
line-height: 1;
}
.event-indicator {
display: flex;
gap: 2px;
margin-top: 2px;
}
.event-dot-mini {
width: 4px;
height: 4px;
border-radius: 50%;
}
.event-dot-mini.work {
background: var(--event-work);
}
.event-dot-mini.personal {
background: var(--event-personal);
}
.event-dot-mini.meeting {
background: var(--event-meeting);
}
.event-dot-mini.important {
background: var(--event-important);
}
.event-dot-mini.other {
background: var(--event-other);
}
/* Section Événements */
.events-section {
border-top: 1px solid #e3e6f0;
padding: 1rem;
background: #f8f9fc;
}
.events-section-title {
font-size: 0.9rem;
font-weight: 700;
color: #5a5c69;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.events-list {
max-height: 300px;
overflow-y: auto;
}
.event-item {
background: white;
padding: 0.75rem;
border-radius: 10px;
margin-bottom: 0.5rem;
border-left: 4px solid;
transition: all 0.2s ease;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.event-item:hover {
transform: translateX(3px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}
.event-item.work {
border-left-color: var(--event-work);
}
.event-item.personal {
border-left-color: var(--event-personal);
}
.event-item.meeting {
border-left-color: var(--event-meeting);
}
.event-item.important {
border-left-color: var(--event-important);
}
.event-item.other {
border-left-color: var(--event-other);
}
.event-item-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.25rem;
}
.event-item-title {
font-weight: 600;
font-size: 0.85rem;
color: #5a5c69;
margin: 0;
}
.event-item-badge {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
border-radius: 12px;
font-weight: 600;
white-space: nowrap;
}
.badge-work {
background: rgba(78, 115, 223, 0.1);
color: #4e73df;
}
.badge-personal {
background: rgba(28, 200, 138, 0.1);
color: #1cc88a;
}
.badge-meeting {
background: rgba(246, 194, 62, 0.1);
color: #f6c23e;
}
.badge-important {
background: rgba(231, 74, 59, 0.1);
color: #e74a3b;
}
.badge-other {
background: rgba(54, 185, 204, 0.1);
color: #36b9cc;
}
.event-item-time {
font-size: 0.75rem;
color: #858796;
margin-bottom: 0.25rem;
}
.event-item-description {
font-size: 0.75rem;
color: #b7b9c9;
margin: 0;
}
.no-events {
text-align: center;
padding: 2rem 1rem;
color: #b7b9c9;
}
.no-events i {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.no-events p {
font-size: 0.85rem;
margin: 0;
}
/* Filtres */
.filter-section {
padding: 0.5rem 1rem;
border-top: 1px solid #e3e6f0;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.btn-filter {
font-size: 0.7rem;
padding: 0.2rem 0.6rem;
border-radius: 15px;
border: 1px solid #e3e6f0;
background: white;
color: #858796;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-filter:hover {
background: #f0f2f5;
}
.btn-filter.active {
background: #4e73df;
color: white;
border-color: #4e73df;
}
/* Tooltip personnalisé */
.event-tooltip {
display: none;
position: absolute;
background: #5a5c69;
color: white;
padding: 0.5rem;
border-radius: 8px;
font-size: 0.75rem;
z-index: 1001;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.day-cell-mini:hover .event-tooltip {
display: block;
}
/* Main Content */
.main-content {
margin-left: var(--sidebar-width);
flex: 1;
padding: 2rem;
}
.main-content h2 {
color: #5a5c69;
margin-bottom: 1.5rem;
}
/* Scrollbar */
.events-list::-webkit-scrollbar {
width: 5px;
}
.events-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.events-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.events-list::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Responsive */
@media (max-width: 992px) {
.sidebar {
width: 320px;
}
:root {
--sidebar-width: 320px;
}
}
@media (max-width: 768px) {
.sidebar {
position: relative;
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid #e3e6f0;
}
.app-container {
flex-direction: column;
}
.main-content {
margin-left: 0;
}
:root {
--sidebar-width: 100%;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Sidebar avec Calendrier Widget -->
<aside class="sidebar">
<!-- En-tête -->
<div class="sidebar-header">
<div class="sidebar-title">
<i class="bi bi-calendar3"></i>
Mon Calendrier
</div>
<div class="sidebar-subtitle">
Événements & Planning
</div>
</div>
<!-- Calendrier Mini -->
<div class="calendar-widget">
<div class="calendar-nav-mini">
<button class="btn-nav-mini" id="prevMonthBtn" title="Mois précédent">
<i class="bi bi-chevron-left"></i>
</button>
<h6 class="month-title-mini" id="miniMonthYear">Mai 2026</h6>
<button class="btn-nav-mini" id="nextMonthBtn" title="Mois suivant">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="weekdays-mini" id="miniWeekdays"></div>
<div class="days-grid-mini" id="miniDaysGrid"></div>
</div>
<!-- Filtres -->
<div class="filter-section">
<div class="filter-buttons">
<button class="btn-filter active" data-filter="all">Tous</button>
<button class="btn-filter" data-filter="work">Travail</button>
<button class="btn-filter" data-filter="personal">Personnel</button>
<button class="btn-filter" data-filter="meeting">Réunions</button>
<button class="btn-filter" data-filter="important">Important</button>
</div>
</div>
<!-- Liste des Événements -->
<div class="events-section">
<div class="events-section-title">
<i class="bi bi-list-ul"></i>
Événements du jour
<span class="badge bg-primary ms-auto" id="eventCount">0</span>
</div>
<div class="events-list" id="eventsList">
<div class="no-events">
<i class="bi bi-calendar-x"></i>
<p>Sélectionnez une date pour voir les événements</p>
</div>
</div>
</div>
</aside>
<!-- Contenu Principal -->
<main class="main-content">
<div class="container-fluid">
<h2>
<i class="bi bi-speedometer2 me-2"></i>
Tableau de Bord
</h2>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-graph-up me-2"></i>
Aperçu des Événements
</h5>
<div class="row mt-3" id="statsCards">
<!-- Généré dynamiquement -->
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-calendar-check me-2"></i>
Prochains Événements
</h5>
<div class="list-group list-group-flush mt-3" id="upcomingEvents">
<!-- Généré dynamiquement -->
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// ============================================
// BASE DE DONNÉES DES ÉVÉNEMENTS (Array d'objets)
// ============================================
const eventsDatabase = [
{
id: 1,
date: '2026-05-06',
title: 'Réunion projet Alpha',
time: '09:00',
description: 'Revue hebdomadaire des avancées du projet',
category: 'meeting',
priority: 'high'
},
{
id: 2,
date: '2026-05-06',
title: 'Déjeuner équipe',
time: '12:30',
description: 'Restaurant Le Petit Bistrot',
category: 'personal',
priority: 'medium'
},
{
id: 3,
date: '2026-05-07',
title: 'Formation React',
time: '14:00',
description: 'Session de formation avancée',
category: 'work',
priority: 'high'
},
{
id: 4,
date: '2026-05-08',
title: 'Deadline rapport',
time: '18:00',
description: 'Soumission finale du rapport Q2',
category: 'important',
priority: 'critical'
},
{
id: 5,
date: '2026-05-10',
title: 'Anniversaire Marie',
time: '20:00',
description: 'Soirée d\'anniversaire',
category: 'personal',
priority: 'medium'
},
{
id: 6,
date: '2026-05-12',
title: 'Conférence Tech',
time: '10:00',
description: 'Présentation des nouvelles technologies',
category: 'work',
priority: 'high'
},
{
id: 7,
date: '2026-05-15',
title: 'Rendez-vous médical',
time: '11:30',
description: 'Consultation annuelle',
category: 'personal',
priority: 'low'
},
{
id: 8,
date: '2026-05-15',
title: 'Sprint planning',
time: '14:00',
description: 'Planification du sprint #23',
category: 'meeting',
priority: 'high'
},
{
id: 9,
date: '2026-05-18',
title: 'Lancement produit',
time: '09:00',
description: 'Mise en production v2.0',
category: 'important',
priority: 'critical'
},
{
id: 10,
date: '2026-05-20',
title: 'Team Building',
time: '10:00',
description: 'Activité de cohésion d\'équipe',
category: 'personal',
priority: 'medium'
},
{
id: 11,
date: '2026-05-22',
title: 'Audit sécurité',
time: '15:00',
description: 'Audit annuel des systèmes',
category: 'work',
priority: 'high'
},
{
id: 12,
date: '2026-05-25',
title: 'Présentation clients',
time: '11:00',
description: 'Démonstration nouveaux modules',
category: 'meeting',
priority: 'high'
},
{
id: 13,
date: '2026-05-26',
title: 'Maintenance serveur',
time: '22:00',
description: 'Mise à jour infrastructure',
category: 'important',
priority: 'critical'
},
{
id: 14,
date: '2026-05-28',
title: 'Afterwork',
time: '18:30',
description: 'Bar Le Central',
category: 'personal',
priority: 'low'
},
{
id: 15,
date: '2026-05-30',
title: 'Bilan mensuel',
time: '16:00',
description: 'Revue des KPIs du mois',
category: 'work',
priority: 'medium'
}
];
// ============================================
// CLASS CALENDRIER WIDGET
// ============================================
class CalendarWidget {
constructor() {
this.currentDate = new Date(2026, 4, 6); // Mai 2026
this.selectedDate = new Date(2026, 4, 6);
this.currentFilter = 'all';
this.events = eventsDatabase;
this.init();
}
init() {
this.renderCalendar();
this.displayEventsForDate(this.selectedDate);
this.updateMainContent();
this.attachListeners();
}
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
getEventsForDate(date) {
const dateStr = this.formatDate(date);
let dateEvents = this.events.filter(event => event.date === dateStr);
if (this.currentFilter !== 'all') {
dateEvents = dateEvents.filter(event => event.category === this.currentFilter);
}
return dateEvents;
}
getEventCountForDate(date) {
return this.getEventsForDate(date).length;
}
getAllEventsForDate(date) {
const dateStr = this.formatDate(date);
return this.events.filter(event => event.date === dateStr);
}
renderCalendar() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// Titre
const options = { month: 'long', year: 'numeric' };
document.getElementById('miniMonthYear').textContent =
this.currentDate.toLocaleDateString('fr-FR', options);
// Jours de la semaine
const weekdays = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
document.getElementById('miniWeekdays').innerHTML = weekdays.map((day, index) => {
const isWeekend = index >= 5;
return `<div class="weekday-mini ${isWeekend ? 'weekend' : ''}">${day}</div>`;
}).join('');
// Grille des jours
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startPadding = (firstDay.getDay() + 6) % 7;
let html = '';
// Jours du mois précédent
const prevLastDay = new Date(year, month, 0);
for (let i = startPadding - 1; i >= 0; i--) {
const day = prevLastDay.getDate() - i;
const date = new Date(year, month - 1, day);
html += this.createDayCell(date, true);
}
// Jours du mois courant
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(year, month, day);
html += this.createDayCell(date, false);
}
// Jours du mois suivant
const remainingCells = 42 - (startPadding + lastDay.getDate());
for (let day = 1; day <= remainingCells; day++) {
const date = new Date(year, month + 1, day);
html += this.createDayCell(date, true);
}
document.getElementById('miniDaysGrid').innerHTML = html;
}
createDayCell(date, isOtherMonth) {
const today = new Date(2026, 4, 6); // Date de référence
const dateStr = this.formatDate(date);
const allEvents = this.getAllEventsForDate(date);
const eventCount = allEvents.length;
const isToday = this.formatDate(date) === this.formatDate(today);
const isSelected = this.formatDate(date) === this.formatDate(this.selectedDate);
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
let classes = ['day-cell-mini'];
if (isOtherMonth) classes.push('other-month');
if (isToday) classes.push('today');
if (isSelected) classes.push('selected');
if (isWeekend && !isToday) classes.push('weekend');
// Indicateurs d'événements
let eventIndicators = '';
if (eventCount > 0) {
const categories = [...new Set(allEvents.map(e => e.category))];
eventIndicators = `
<div class="event-indicator">
${categories.slice(0, 3).map(cat =>
`<span class="event-dot-mini ${cat}"></span>`
).join('')}
${categories.length > 3 ? '<span class="event-dot-mini other">+</span>' : ''}
</div>
`;
}
// Tooltip
let tooltip = '';
if (eventCount > 0) {
const tooltipEvents = allEvents.slice(0, 3).map(e => e.title).join('<br>');
const moreText = allEvents.length > 3 ? `<br>+${allEvents.length - 3} autres...` : '';
tooltip = `
<div class="event-tooltip">
${tooltipEvents}${moreText}
</div>
`;
}
return `
<div class="${classes.join(' ')}"
data-date="${dateStr}"
onclick="calendarWidget.selectDate('${dateStr}')"
title="${eventCount} événement(s)">
<span class="day-number-mini">${date.getDate()}</span>
${eventIndicators}
${tooltip}
</div>
`;
}
selectDate(dateStr) {
this.selectedDate = new Date(dateStr + 'T00:00:00');
this.renderCalendar();
this.displayEventsForDate(this.selectedDate);
}
displayEventsForDate(date) {
const events = this.getEventsForDate(date);
const eventsList = document.getElementById('eventsList');
const eventCount = document.getElementById('eventCount');
const dateFormatted = date.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long'
});
document.querySelector('.events-section-title').innerHTML = `
<i class="bi bi-list-ul"></i>
${dateFormatted}
<span class="badge bg-primary ms-auto" id="eventCount">${events.length}</span>
`;
if (events.length === 0) {
eventsList.innerHTML = `
<div class="no-events">
<i class="bi bi-calendar-check"></i>
<p>Aucun événement pour cette date</p>
</div>
`;
return;
}
// Trier par heure
const sortedEvents = events.sort((a, b) => {
if (!a.time) return 1;
if (!b.time) return -1;
return a.time.localeCompare(b.time);
});
const categoryLabels = {
work: 'Travail',
personal: 'Personnel',
meeting: 'Réunion',
important: 'Important',
other: 'Autre'
};
const priorityIcons = {
low: '',
medium: '<i class="bi bi-flag text-warning ms-1"></i>',
high: '<i class="bi bi-flag text-danger ms-1"></i>',
critical: '<i class="bi bi-exclamation-triangle text-danger ms-1"></i>'
};
eventsList.innerHTML = sortedEvents.map(event => `
<div class="event-item ${event.category}" onclick="calendarWidget.highlightEvent(${event.id})">
<div class="event-item-header">
<h6 class="event-item-title">
${event.title}
${priorityIcons[event.priority] || ''}
</h6>
<span class="event-item-badge badge-${event.category}">
${categoryLabels[event.category] || event.category}
</span>
</div>
<div class="event-item-time">
<i class="bi bi-clock me-2"></i>
${event.time || 'Journée entière'}
</div>
${event.description ? `
<p class="event-item-description">
<i class="bi bi-chat-left-text me-2"></i>
${event.description}
</p>
` : ''}
</div>
`).join('');
}
highlightEvent(eventId) {
const event = this.events.find(e => e.id === eventId);
if (!event) return;
// Animation de surbrillance
const eventItems = document.querySelectorAll('.event-item');
eventItems.forEach(item => {
item.style.transform = 'scale(1)';
item.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
});
const selectedItem = eventItems[this.getEventsForDate(this.selectedDate).findIndex(e => e.id === eventId)];
if (selectedItem) {
selectedItem.style.transform = 'scale(1.02)';
selectedItem.style.boxShadow = '0 5px 20px rgba(78,115,223,0.3)';
}
}
filterEvents(category) {
this.currentFilter = category;
// Mise à jour des boutons
document.querySelectorAll('.btn-filter').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.filter === category) {
btn.classList.add('active');
}
});
this.renderCalendar();
this.displayEventsForDate(this.selectedDate);
}
navigateMonth(direction) {
this.currentDate.setMonth(this.currentDate.getMonth() + direction);
this.renderCalendar();
}
updateMainContent() {
// Statistiques
const totalEvents = this.events.length;
const workEvents = this.events.filter(e => e.category === 'work').length;
const personalEvents = this.events.filter(e => e.category === 'personal').length;
const meetingEvents = this.events.filter(e => e.category === 'meeting').length;
const importantEvents = this.events.filter(e => e.category === 'important').length;
document.getElementById('statsCards').innerHTML = `
<div class="col-6 mb-3">
<div class="card bg-primary text-white">
<div class="card-body text-center py-3">
<h3 class="mb-0">${totalEvents}</h3>
<small>TOTAL</small>
</div>
</div>
</div>
<div class="col-6 mb-3">
<div class="card bg-success text-white">
<div class="card-body text-center py-3">
<h3 class="mb-0">${workEvents}</h3>
<small>TRAVAIL</small>
</div>
</div>
</div>
<div class="col-6">
<div class="card bg-warning">
<div class="card-body text-center py-3">
<h3 class="mb-0">${meetingEvents}</h3>
<small>RÉUNIONS</small>
</div>
</div>
</div>
<div class="col-6">
<div class="card bg-danger text-white">
<div class="card-body text-center py-3">
<h3 class="mb-0">${importantEvents}</h3>
<small>IMPORTANT</small>
</div>
</div>
</div>
`;
// Prochains événements
const today = new Date(2026, 4, 6);
const upcomingEvents = this.events
.filter(e => new Date(e.date + 'T00:00:00') >= today)
.sort((a, b) => new Date(a.date + 'T00:00:00') - new Date(b.date + 'T00:00:00'))
.slice(0, 5);
document.getElementById('upcomingEvents').innerHTML = upcomingEvents.map(event => {
const eventDate = new Date(event.date + 'T00:00:00');
const daysUntil = Math.ceil((eventDate - today) / (1000 * 60 * 60 * 24));
const dayLabel = daysUntil === 0 ? "Aujourd'hui" :
daysUntil === 1 ? "Demain" :
`Dans ${daysUntil} jours`;
return `
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${event.title}</h6>
<small class="text-muted">
${eventDate.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short' })}
${event.time ? ` à ${event.time}` : ''}
</small>
</div>
<span class="badge bg-secondary rounded-pill">${dayLabel}</span>
</div>
`;
}).join('');
}
attachListeners() {
document.getElementById('prevMonthBtn').addEventListener('click', () => {
this.navigateMonth(-1);
});
document.getElementById('nextMonthBtn').addEventListener('click', () => {
this.navigateMonth(1);
});
// Filtres
document.querySelectorAll('.btn-filter').forEach(btn => {
btn.addEventListener('click', (e) => {
this.filterEvents(e.target.dataset.filter);
});
});
// Mise à jour périodique (optionnel)
setInterval(() => {
this.updateMainContent();
}, 60000); // Toutes les minutes
}
}
// Initialisation du widget
const calendarWidget = new CalendarWidget();
</script>
</body>
</html>
Télécharger le fichier source