Files
calendar/frontend/src/components/event_modal.rs
Connor Johnstone e56253b9c2 Fix all-day recurring events RFC-5545 compliance
- Set all_day flag properly when creating VEvent in series handler
- Improve all-day event detection using VALUE=DATE parameter
- Add RFC-5545 compliance for exclusive end dates (backend adds 1 day)
- Fix end date display in event modal (frontend subtracts 1 day for display)
- Fix recurring all-day event expansion to maintain proper end date pattern

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 12:21:46 -04:00

250 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::models::ical::VEvent;
use chrono::{DateTime, Utc};
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct EventModalProps {
pub event: Option<VEvent>,
pub on_close: Callback<()>,
}
#[function_component]
pub fn EventModal(props: &EventModalProps) -> Html {
let close_modal = {
let on_close = props.on_close.clone();
Callback::from(move |_| {
on_close.emit(());
})
};
let backdrop_click = {
let on_close = props.on_close.clone();
Callback::from(move |e: MouseEvent| {
if e.target() == e.current_target() {
on_close.emit(());
}
})
};
if let Some(ref event) = props.event {
html! {
<div class="modal-backdrop" onclick={backdrop_click}>
<div class="modal-content">
<div class="modal-header">
<h3>{"Event Details"}</h3>
<button class="modal-close" onclick={close_modal}>{"×"}</button>
</div>
<div class="modal-body">
<div class="event-detail">
<strong>{"Title:"}</strong>
<span>{event.get_title()}</span>
</div>
{
if let Some(ref description) = event.description {
html! {
<div class="event-detail">
<strong>{"Description:"}</strong>
<span>{description}</span>
</div>
}
} else {
html! {}
}
}
<div class="event-detail">
<strong>{"Start:"}</strong>
<span>{format_datetime(&event.dtstart, event.all_day)}</span>
</div>
{
if let Some(ref end) = event.dtend {
html! {
<div class="event-detail">
<strong>{"End:"}</strong>
<span>{format_datetime_end(end, event.all_day)}</span>
</div>
}
} else {
html! {}
}
}
<div class="event-detail">
<strong>{"All Day:"}</strong>
<span>{if event.all_day { "Yes" } else { "No" }}</span>
</div>
{
if let Some(ref location) = event.location {
html! {
<div class="event-detail">
<strong>{"Location:"}</strong>
<span>{location}</span>
</div>
}
} else {
html! {}
}
}
<div class="event-detail">
<strong>{"Status:"}</strong>
<span>{event.get_status_display()}</span>
</div>
<div class="event-detail">
<strong>{"Privacy:"}</strong>
<span>{event.get_class_display()}</span>
</div>
<div class="event-detail">
<strong>{"Priority:"}</strong>
<span>{event.get_priority_display()}</span>
</div>
{
if let Some(ref organizer) = event.organizer {
html! {
<div class="event-detail">
<strong>{"Organizer:"}</strong>
<span>{organizer.cal_address.clone()}</span>
</div>
}
} else {
html! {}
}
}
{
if !event.attendees.is_empty() {
html! {
<div class="event-detail">
<strong>{"Attendees:"}</strong>
<span>{event.attendees.iter().map(|a| a.cal_address.clone()).collect::<Vec<_>>().join(", ")}</span>
</div>
}
} else {
html! {}
}
}
{
if !event.categories.is_empty() {
html! {
<div class="event-detail">
<strong>{"Categories:"}</strong>
<span>{event.categories.join(", ")}</span>
</div>
}
} else {
html! {}
}
}
{
if let Some(ref recurrence) = event.rrule {
html! {
<div class="event-detail">
<strong>{"Repeats:"}</strong>
<span>{format_recurrence_rule(recurrence)}</span>
</div>
}
} else {
html! {
<div class="event-detail">
<strong>{"Repeats:"}</strong>
<span>{"No"}</span>
</div>
}
}
}
{
if !event.alarms.is_empty() {
html! {
<div class="event-detail">
<strong>{"Reminders:"}</strong>
<span>{"Alarms configured"}</span> /* TODO: Convert VAlarm to displayable format */
</div>
}
} else {
html! {
<div class="event-detail">
<strong>{"Reminders:"}</strong>
<span>{"None"}</span>
</div>
}
}
}
{
if let Some(ref created) = event.created {
html! {
<div class="event-detail">
<strong>{"Created:"}</strong>
<span>{format_datetime(created, false)}</span>
</div>
}
} else {
html! {}
}
}
{
if let Some(ref modified) = event.last_modified {
html! {
<div class="event-detail">
<strong>{"Last Modified:"}</strong>
<span>{format_datetime(modified, false)}</span>
</div>
}
} else {
html! {}
}
}
</div>
</div>
</div>
}
} else {
html! {}
}
}
fn format_datetime(dt: &DateTime<Utc>, all_day: bool) -> String {
if all_day {
dt.format("%B %d, %Y").to_string()
} else {
dt.format("%B %d, %Y at %I:%M %p").to_string()
}
}
fn format_datetime_end(dt: &DateTime<Utc>, all_day: bool) -> String {
if all_day {
// For all-day events, subtract one day from end date for display
// RFC-5545 uses exclusive end dates, but users expect inclusive display
let display_date = *dt - chrono::Duration::days(1);
display_date.format("%B %d, %Y").to_string()
} else {
dt.format("%B %d, %Y at %I:%M %p").to_string()
}
}
fn format_recurrence_rule(rrule: &str) -> String {
// Basic parsing of RRULE to display user-friendly text
if rrule.contains("FREQ=DAILY") {
"Daily".to_string()
} else if rrule.contains("FREQ=WEEKLY") {
"Weekly".to_string()
} else if rrule.contains("FREQ=MONTHLY") {
"Monthly".to_string()
} else if rrule.contains("FREQ=YEARLY") {
"Yearly".to_string()
} else {
// Show the raw rule if we can't parse it
format!("Custom ({})", rrule)
}
}