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 {}
|
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) {
|
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||||
tera.register_function("now", NowFunction);
|
tera.register_function("now", NowFunction);
|
||||||
tera.register_function("format_date", FormatDateFunction);
|
tera.register_function("format_date", FormatDateFunction);
|
||||||
tera.register_function("local_time", LocalTimeFunction);
|
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
|
/// 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
|
/// Formats a date for display
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
||||||
|
@ -4,47 +4,125 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h1>Calendar</h1>
|
<!-- Calendar Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
<p>View Mode: {{ view_mode }}</p>
|
<div class="col-12">
|
||||||
<p>Current Date: {{ current_date }}</p>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<h1 class="h3 mb-1">Calendar</h1>
|
||||||
<div class="btn-group">
|
<p class="text-muted mb-0">Manage your events and schedule</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<a href="/calendar/new" class="btn btn-success">
|
<div>
|
||||||
<i class="bi bi-plus-circle"></i> Create New Event
|
<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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if view_mode == "month" %}
|
{% if view_mode == "month" %}
|
||||||
<h2>Month View: {{ month_name }} {{ current_year }}</h2>
|
<!-- Month View -->
|
||||||
|
<div class="card">
|
||||||
<table class="table table-bordered">
|
<div class="card-header bg-primary text-white">
|
||||||
<thead>
|
<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>
|
<tr>
|
||||||
<th>Sun</th>
|
<th class="text-center py-3">Sunday</th>
|
||||||
<th>Mon</th>
|
<th class="text-center py-3">Monday</th>
|
||||||
<th>Tue</th>
|
<th class="text-center py-3">Tuesday</th>
|
||||||
<th>Wed</th>
|
<th class="text-center py-3">Wednesday</th>
|
||||||
<th>Thu</th>
|
<th class="text-center py-3">Thursday</th>
|
||||||
<th>Fri</th>
|
<th class="text-center py-3">Friday</th>
|
||||||
<th>Sat</th>
|
<th class="text-center py-3">Saturday</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for week in range(start=0, end=6) %}
|
{% for week in range(start=0, end=6) %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for day_idx in range(start=0, end=7) %}
|
{% for day_idx in range(start=0, end=7) %}
|
||||||
<td>
|
|
||||||
{% set idx = week * 7 + day_idx %}
|
{% set idx = week * 7 + day_idx %}
|
||||||
{% if idx < calendar_days|length %}
|
<td class="calendar-day" onclick="openEventModalForDate(this)" {% if idx <
|
||||||
{% set day = calendar_days[idx] %}
|
calendar_days|length %} data-day="{{ calendar_days[idx].day }}"
|
||||||
{% if day.day > 0 %}
|
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 }}
|
{{ 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 %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if day.events|length > 2 %}
|
||||||
|
<div class="small text-muted text-center">+{{ day.events|length - 2 }} more</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@ -53,25 +131,59 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% elif view_mode == "year" %}
|
</div>
|
||||||
<h2>Year View: {{ current_year }}</h2>
|
</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">
|
<div class="row">
|
||||||
{% for month in months %}
|
{% for month in months %}
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
|
||||||
<div class="card">
|
<div class="card h-100 shadow-sm">
|
||||||
<div class="card-header">{{ month.name }}</div>
|
<div class="card-header bg-light">
|
||||||
<div class="card-body">
|
<h6 class="mb-0 text-center">{{ month.name }}</h6>
|
||||||
<p>Events: {{ month.events|length }}</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% elif view_mode == "day" %}
|
</div>
|
||||||
<h2>Day View: {{ current_date }}</h2>
|
</div>
|
||||||
|
{% elif view_mode == "day" %}
|
||||||
|
<h2>Day View: {{ current_date }}</h2>
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-primary text-white">
|
||||||
All Day Events
|
All Day Events
|
||||||
</div>
|
</div>
|
||||||
@ -89,24 +201,24 @@
|
|||||||
<p class="text-muted">No all-day events</p>
|
<p class="text-muted">No all-day events</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for hour in range(start=0, end=24) %}
|
{% for hour in range(start=0, end=24) %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="pe-3" style="width: 60px; text-align: right;">
|
<div class="pe-3" style="width: 60px; text-align: right;">
|
||||||
<strong>{{ "%02d"|format(value=hour) }}:00</strong>
|
<strong>{{ hour|format_hour }}:00</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
{% if events is defined and events|length > 0 %}
|
{% if events is defined and events|length > 0 %}
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
{% if not event.all_day %}
|
{% if not event.all_day %}
|
||||||
{% set start_hour = event.start_time|date(format="%H") %}
|
{% set start_hour = event.start_time|extract_hour %}
|
||||||
{% if start_hour == hour|string %}
|
{% if start_hour == hour|string %}
|
||||||
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
|
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
|
||||||
<h5>{{ event.title }}</h5>
|
<h5>{{ event.title }}</h5>
|
||||||
<p>{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}</p>
|
<p>{{ event.start_time|format_time }} - {{ event.end_time|format_time }}</p>
|
||||||
<p>{{ event.description }}</p>
|
<p>{{ event.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -117,16 +229,408 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Floating Action Button (FAB) for creating new events -->
|
<!-- New Event Modal -->
|
||||||
<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;">
|
<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="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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
<i class="bi bi-plus"></i>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.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 %}
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user