- 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>
250 lines
9.7 KiB
Rust
250 lines
9.7 KiB
Rust
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)
|
||
}
|
||
}
|