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:
parent
2827cfebc9
commit
d815d9d365
0
actix_mvc_app/src/db/calendar.rs
Normal file
0
actix_mvc_app/src/db/calendar.rs
Normal 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 {
|
||||
|
@ -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 %}
|
Loading…
Reference in New Issue
Block a user