feat: Add custom Tera filters for date/time formatting

- Add three new Tera filters: `format_hour`, `extract_hour`, and
  `format_time` for flexible date/time formatting in templates.
- Improve template flexibility and maintainability by allowing
  customizable date/time display.
- Enhance the user experience with more precise date/time rendering.
This commit is contained in:
Mahmoud-Emad 2025-05-28 10:43:02 +03:00
parent 2827cfebc9
commit d815d9d365
3 changed files with 671 additions and 99 deletions

View File

View File

@ -26,11 +26,16 @@ impl std::fmt::Display for TemplateError {
impl std::error::Error for TemplateError {}
/// Registers custom Tera functions
/// Registers custom Tera functions and filters
pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction);
tera.register_function("format_date", FormatDateFunction);
tera.register_function("local_time", LocalTimeFunction);
// Register custom filters
tera.register_filter("format_hour", format_hour_filter);
tera.register_filter("extract_hour", extract_hour_filter);
tera.register_filter("format_time", format_time_filter);
}
/// Tera function to get the current date/time
@ -140,6 +145,69 @@ impl Function for LocalTimeFunction {
}
}
/// Tera filter to format hour with zero padding
pub fn format_hour_filter(
value: &Value,
_args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
match value.as_i64() {
Some(hour) => Ok(Value::String(format!("{:02}", hour))),
None => Err(tera::Error::msg("Value must be a number")),
}
}
/// Tera filter to extract hour from datetime string
pub fn extract_hour_filter(
value: &Value,
_args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
match value.as_str() {
Some(datetime_str) => {
// Try to parse as RFC3339 first
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
Ok(Value::String(dt.format("%H").to_string()))
} else {
// Try to parse as our standard format
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
Ok(dt) => Ok(Value::String(dt.format("%H").to_string())),
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
}
}
}
None => Err(tera::Error::msg("Value must be a string")),
}
}
/// Tera filter to format time from datetime string
pub fn format_time_filter(
value: &Value,
args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
None => "%H:%M",
},
None => "%H:%M",
};
match value.as_str() {
Some(datetime_str) => {
// Try to parse as RFC3339 first
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
Ok(Value::String(dt.format(format).to_string()))
} else {
// Try to parse as our standard format
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
Ok(dt) => Ok(Value::String(dt.format(format).to_string())),
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
}
}
}
None => Err(tera::Error::msg("Value must be a string")),
}
}
/// Formats a date for display
#[allow(dead_code)]
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {

View File

@ -4,129 +4,633 @@
{% block content %}
<div class="container-fluid">
<h1>Calendar</h1>
<p>View Mode: {{ view_mode }}</p>
<p>Current Date: {{ current_date }}</p>
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="btn-group">
<a href="/calendar?view=day" class="btn btn-outline-primary">Day</a>
<a href="/calendar?view=month" class="btn btn-outline-primary">Month</a>
<a href="/calendar?view=year" class="btn btn-outline-primary">Year</a>
<!-- Calendar Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-1">Calendar</h1>
<p class="text-muted mb-0">Manage your events and schedule</p>
</div>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#newEventModal">
<i class="bi bi-plus-circle"></i> Create Event
</button>
</div>
</div>
</div>
</div>
<!-- Calendar Navigation -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group" role="group" aria-label="Calendar view modes">
<button id="todayBtn" onclick="goToToday()" class="btn btn-outline-success">
<i class="bi bi-calendar-check"></i> Today
</button>
<a href="/calendar?view=day"
class="btn {% if view_mode == 'day' %}btn-primary{% else %}btn-outline-primary{% endif %}">
<i class="bi bi-calendar-day"></i> Day
</a>
<a href="/calendar?view=month"
class="btn {% if view_mode == 'month' %}btn-primary{% else %}btn-outline-primary{% endif %}">
<i class="bi bi-calendar3"></i> Month
</a>
<a href="/calendar?view=year"
class="btn {% if view_mode == 'year' %}btn-primary{% else %}btn-outline-primary{% endif %}">
<i class="bi bi-calendar4-range"></i> Year
</a>
</div>
<div class="text-muted">
<i class="bi bi-calendar-event"></i> {{ current_date }}
</div>
</div>
{% if view_mode == "month" %}
<div class="text-center month-nav-hint">
<small class="text-muted">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i> Use arrow keys to navigate months
| Click on any day to create an event
</small>
</div>
{% endif %}
</div>
<a href="/calendar/new" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Create New Event
</a>
</div>
{% if view_mode == "month" %}
<h2>Month View: {{ month_name }} {{ current_year }}</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>Sun</th>
<th>Mon</th>
<th>Tue</th>
<th>Wed</th>
<th>Thu</th>
<th>Fri</th>
<th>Sat</th>
</tr>
</thead>
<tbody>
{% for week in range(start=0, end=6) %}
<tr>
{% for day_idx in range(start=0, end=7) %}
<td>
{% set idx = week * 7 + day_idx %}
{% if idx < calendar_days|length %}
{% set day = calendar_days[idx] %}
{% if day.day > 0 %}
{{ day.day }}
<!-- Month View -->
<div class="card">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center">
<button class="btn btn-outline-light btn-sm" onclick="navigateMonth(-1)">
<i class="bi bi-chevron-left"></i>
</button>
<h4 class="mb-0">
<i class="bi bi-calendar3"></i> {{ month_name }} {{ current_year }}
</h4>
<button class="btn btn-outline-light btn-sm" onclick="navigateMonth(1)">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-bordered mb-0 calendar-table">
<thead class="table-light">
<tr>
<th class="text-center py-3">Sunday</th>
<th class="text-center py-3">Monday</th>
<th class="text-center py-3">Tuesday</th>
<th class="text-center py-3">Wednesday</th>
<th class="text-center py-3">Thursday</th>
<th class="text-center py-3">Friday</th>
<th class="text-center py-3">Saturday</th>
</tr>
</thead>
<tbody>
{% for week in range(start=0, end=6) %}
<tr>
{% for day_idx in range(start=0, end=7) %}
{% set idx = week * 7 + day_idx %}
<td class="calendar-day" onclick="openEventModalForDate(this)" {% if idx <
calendar_days|length %} data-day="{{ calendar_days[idx].day }}"
data-is-current-month="{{ calendar_days[idx].is_current_month }}"
data-date="{{ current_year }}-{{ current_month|format_hour }}-{{ calendar_days[idx].day|format_hour }}"
{% else %} data-day="0" data-is-current-month="false" data-date="" {% endif %}>
{% if idx < calendar_days|length %} {% set day=calendar_days[idx] %} {% if day.day> 0 %}
<div
class="calendar-day-header d-flex justify-content-between align-items-center mb-2">
<span
class="calendar-day-number {% if day.is_current_month %}text-dark{% else %}text-muted{% endif %}">
{{ day.day }}
</span>
{% if day.events|length > 0 %}
<small class="badge bg-primary">{{ day.events|length }}</small>
{% endif %}
</div>
{% if day.events|length > 0 %}
<div class="calendar-events">
{% for event in day.events %}
{% if loop.index <= 2 %} <div class="event-preview text-truncate mb-1"
style="background-color: {{ event.color }}; color: white;">
{{ event.title }}
</div>
{% endif %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
{% if day.events|length > 2 %}
<div class="small text-muted text-center">+{{ day.events|length - 2 }} more</div>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% elif view_mode == "year" %}
<h2>Year View: {{ current_year }}</h2>
</table>
</div>
</div>
</div>
{% elif view_mode == "year" %}
<!-- Year View -->
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="bi bi-calendar4-range"></i> Year {{ current_year }}
</h4>
</div>
<div class="card-body">
<div class="row">
{% for month in months %}
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">{{ month.name }}</div>
<div class="card-body">
<p>Events: {{ month.events|length }}</p>
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-header bg-light">
<h6 class="mb-0 text-center">{{ month.name }}</h6>
</div>
<div class="card-body text-center">
{% if month.events|length > 0 %}
<div class="mb-2">
<span class="badge bg-primary fs-6">{{ month.events|length }}</span>
</div>
<p class="text-muted small mb-0">
{% if month.events|length == 1 %}
1 event
{% else %}
{{ month.events|length }} events
{% endif %}
</p>
<div class="mt-2">
<a href="/calendar?view=month" class="btn btn-sm btn-outline-primary">
View Month
</a>
</div>
{% else %}
<div class="text-muted">
<i class="bi bi-calendar-x fs-4 mb-2 d-block"></i>
<p class="small mb-0">No events</p>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% elif view_mode == "day" %}
<h2>Day View: {{ current_date }}</h2>
</div>
</div>
{% elif view_mode == "day" %}
<h2>Day View: {{ current_date }}</h2>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
All Day Events
<div class="card mb-4">
<div class="card-header bg-primary text-white">
All Day Events
</div>
<div class="card-body">
{% if events is defined and events|length > 0 %}
{% for event in events %}
{% if event.all_day %}
<div class="alert" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endfor %}
{% else %}
<p class="text-muted">No all-day events</p>
{% endif %}
</div>
</div>
<div class="list-group">
{% for hour in range(start=0, end=24) %}
<div class="list-group-item">
<div class="d-flex">
<div class="pe-3" style="width: 60px; text-align: right;">
<strong>{{ hour|format_hour }}:00</strong>
</div>
<div class="card-body">
<div class="flex-grow-1">
{% if events is defined and events|length > 0 %}
{% for event in events %}
{% if event.all_day %}
<div class="alert" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endfor %}
{% else %}
<p class="text-muted">No all-day events</p>
{% for event in events %}
{% if not event.all_day %}
{% set start_hour = event.start_time|extract_hour %}
{% if start_hour == hour|string %}
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.start_time|format_time }} - {{ event.end_time|format_time }}</p>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="list-group">
{% for hour in range(start=0, end=24) %}
<div class="list-group-item">
<div class="d-flex">
<div class="pe-3" style="width: 60px; text-align: right;">
<strong>{{ "%02d"|format(value=hour) }}:00</strong>
<!-- New Event Modal -->
<div class="modal fade" id="newEventModal" tabindex="-1" aria-labelledby="newEventModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="newEventModalLabel">
<i class="bi bi-plus-circle"></i> Create New Event
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form id="newEventForm" action="/calendar/events" method="post">
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="eventTitle" class="form-label">Event Title *</label>
<input type="text" class="form-control" id="eventTitle" name="title" required>
</div>
</div>
<div class="flex-grow-1">
{% if events is defined and events|length > 0 %}
{% for event in events %}
{% if not event.all_day %}
{% set start_hour = event.start_time|date(format="%H") %}
{% if start_hour == hour|string %}
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}</p>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
<div class="col-md-4">
<div class="mb-3">
<label for="eventColor" class="form-label">Color</label>
<select class="form-select" id="eventColor" name="color">
<option value="#4285F4" selected>Blue</option>
<option value="#EA4335">Red</option>
<option value="#34A853">Green</option>
<option value="#FBBC05">Yellow</option>
<option value="#A142F4">Purple</option>
<option value="#24C1E0">Cyan</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="eventDescription" class="form-label">Description</label>
<textarea class="form-control" id="eventDescription" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="allDayEvent" name="all_day">
<label class="form-check-label" for="allDayEvent">All Day Event</label>
</div>
</div>
<div class="row" id="timeInputs">
<div class="col-md-6">
<div class="mb-3">
<label for="startTime" class="form-label">Start Time *</label>
<input type="datetime-local" class="form-control" id="startTime" name="start_time"
required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="endTime" class="form-label">End Time *</label>
<input type="datetime-local" class="form-control" id="endTime" name="end_time" required>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Create Event
</button>
</div>
</form>
</div>
{% endif %}
</div>
</div>
<!-- Floating Action Button (FAB) for creating new events -->
<a href="/calendar/new" class="position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow" style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
<i class="bi bi-plus"></i>
</a>
<!-- Floating Action Button (FAB) for mobile -->
<button type="button" class="d-md-none position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow"
data-bs-toggle="modal" data-bs-target="#newEventModal"
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
<i class="bi bi-plus"></i>
</button>
</div>
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
.calendar-table {
border: 2px solid #dee2e6;
}
.calendar-table td {
cursor: pointer;
transition: background-color 0.2s ease;
border: 1px solid #dee2e6 !important;
text-align: center;
vertical-align: top;
position: relative;
}
.calendar-table td:hover {
background-color: #e3f2fd;
border-color: #2196f3 !important;
}
.calendar-day {
min-height: 120px;
position: relative;
padding: 30px !important;
}
.calendar-day-header {
width: 100%;
margin-bottom: 8px;
}
.calendar-day-number {
font-weight: bold;
font-size: 1.1rem;
flex-grow: 1;
text-align: center;
}
.calendar-day[data-is-current-month="true"]:hover::after {
content: "Click to add event";
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
background: rgba(33, 150, 243, 0.9);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
}
.calendar-day[data-is-current-month="false"] {
background-color: #f8f9fa;
color: #6c757d;
}
.calendar-day.today {
background-color: #e3f2fd;
border: 2px solid #2196f3 !important;
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
}
.calendar-day.today .calendar-day-number {
color: #1976d2;
font-weight: 900;
}
.calendar-events {
text-align: left;
}
.event-preview {
font-size: 0.7rem;
padding: 2px 4px;
margin: 1px 0;
border-radius: 2px;
cursor: pointer;
transition: opacity 0.2s;
font-weight: 500;
}
.event-preview:hover {
opacity: 0.8;
}
/* Month navigation buttons */
.btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
/* Keyboard navigation hint */
.month-nav-hint {
font-size: 0.8rem;
opacity: 0.7;
margin-top: 0.5rem;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
// Handle all-day event checkbox
document.getElementById('allDayEvent').addEventListener('change', function () {
const timeInputs = document.getElementById('timeInputs');
const startTime = document.getElementById('startTime');
const endTime = document.getElementById('endTime');
if (this.checked) {
timeInputs.style.display = 'none';
startTime.removeAttribute('required');
endTime.removeAttribute('required');
} else {
timeInputs.style.display = 'block';
startTime.setAttribute('required', '');
endTime.setAttribute('required', '');
}
});
// Handle form submission
document.getElementById('newEventForm').addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(this);
const allDay = document.getElementById('allDayEvent').checked;
if (!allDay) {
// Convert datetime-local to RFC3339 format
const startTime = document.getElementById('startTime').value;
const endTime = document.getElementById('endTime').value;
if (startTime) {
formData.set('start_time', new Date(startTime).toISOString());
}
if (endTime) {
formData.set('end_time', new Date(endTime).toISOString());
}
} else {
// For all-day events, set times to start and end of day
const today = new Date();
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);
formData.set('start_time', startOfDay.toISOString());
formData.set('end_time', endOfDay.toISOString());
}
// Submit the form
fetch(this.action, {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('Error creating event. Please try again.');
}
}).catch(error => {
console.error('Error:', error);
alert('Error creating event. Please try again.');
});
});
// Delete event function
function deleteEvent(eventId) {
if (confirm('Are you sure you want to delete this event?')) {
fetch(`/calendar/events/${eventId}/delete`, {
method: 'POST'
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('Error deleting event. Please try again.');
}
}).catch(error => {
console.error('Error:', error);
alert('Error deleting event. Please try again.');
});
}
}
// Navigate to today
function goToToday() {
const today = new Date().toISOString().split('T')[0];
window.location.href = `/calendar?view=month&date=${today}`;
}
// Navigate between months
function navigateMonth(direction) {
const urlParams = new URLSearchParams(window.location.search);
const currentDate = urlParams.get('date') || new Date().toISOString().split('T')[0];
const date = new Date(currentDate);
// Add or subtract months
date.setMonth(date.getMonth() + direction);
// Format the new date
const newDate = date.toISOString().split('T')[0];
// Navigate to the new month
window.location.href = `/calendar?view=month&date=${newDate}`;
}
// Open event modal for specific date
function openEventModalForDate(dayCell) {
const day = dayCell.getAttribute('data-day');
const isCurrentMonth = dayCell.getAttribute('data-is-current-month') === 'true';
// Only proceed if it's a valid day in the current month
if (day > 0 && isCurrentMonth) {
// Get current month and year from URL or current date
const urlParams = new URLSearchParams(window.location.search);
const currentDate = urlParams.get('date') || new Date().toISOString().split('T')[0];
const date = new Date(currentDate);
// Set the date for the selected day
const selectedDate = new Date(date.getFullYear(), date.getMonth(), parseInt(day));
// Set default times for the selected date
const startTime = new Date(selectedDate);
startTime.setHours(9, 0, 0, 0);
const endTime = new Date(selectedDate);
endTime.setHours(10, 0, 0, 0);
// Update the modal form with the selected date
document.getElementById('startTime').value = startTime.toISOString().slice(0, 16);
document.getElementById('endTime').value = endTime.toISOString().slice(0, 16);
// Update modal title to show the selected date
const modalTitle = document.getElementById('newEventModalLabel');
const dateStr = selectedDate.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create Event for ${dateStr}`;
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
modal.show();
}
}
// Initialize calendar features
function initializeCalendar() {
// Highlight today's date
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
// Find and highlight today's cell
const calendarCells = document.querySelectorAll('.calendar-day[data-date]');
calendarCells.forEach(cell => {
const cellDate = cell.getAttribute('data-date');
if (cellDate === todayStr && cell.getAttribute('data-is-current-month') === 'true') {
cell.classList.add('today');
}
});
// Manage Today button state
const todayBtn = document.getElementById('todayBtn');
if (todayBtn) {
const urlParams = new URLSearchParams(window.location.search);
const currentDate = urlParams.get('date') || today.toISOString().split('T')[0];
const currentMonth = new Date(currentDate).getMonth();
const currentYear = new Date(currentDate).getFullYear();
const todayMonth = today.getMonth();
const todayYear = today.getFullYear();
// Disable Today button if we're already viewing the current month
if (currentMonth === todayMonth && currentYear === todayYear) {
todayBtn.disabled = true;
todayBtn.classList.remove('btn-outline-success');
todayBtn.classList.add('btn-secondary');
todayBtn.title = 'Already viewing current month';
} else {
todayBtn.disabled = false;
todayBtn.classList.remove('btn-secondary');
todayBtn.classList.add('btn-outline-success');
todayBtn.title = 'Go to current month';
}
}
}
// Set default date/time for new events
document.addEventListener('DOMContentLoaded', function () {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0);
const tomorrowEnd = new Date(tomorrow);
tomorrowEnd.setHours(10, 0, 0, 0);
document.getElementById('startTime').value = tomorrow.toISOString().slice(0, 16);
document.getElementById('endTime').value = tomorrowEnd.toISOString().slice(0, 16);
// Initialize calendar features
initializeCalendar();
// Add keyboard navigation for month view
document.addEventListener('keydown', function (e) {
if (window.location.search.includes('view=month') || (!window.location.search.includes('view=') && window.location.pathname === '/calendar')) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
navigateMonth(-1);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
navigateMonth(1);
}
}
});
});
</script>
{% endblock %}
{% endblock %}