Add weekday selection for weekly recurrence and fix RRULE generation
- Add weekday selection UI for weekly recurring events with checkboxes - Implement BYDAY parameter generation in RRULE based on selected days - Fix missing RRULE generation in iCalendar output - Convert reminder durations to proper EventReminder structs - Add responsive CSS styling for weekday selection interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -866,6 +866,11 @@ impl CalDAVClient {
|
|||||||
ical.push_str("END:VALARM\r\n");
|
ical.push_str("END:VALARM\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recurrence rule
|
||||||
|
if let Some(rrule) = &event.recurrence_rule {
|
||||||
|
ical.push_str(&format!("RRULE:{}\r\n", rrule));
|
||||||
|
}
|
||||||
|
|
||||||
ical.push_str("END:VEVENT\r\n");
|
ical.push_str("END:VEVENT\r\n");
|
||||||
ical.push_str("END:VCALENDAR\r\n");
|
ical.push_str("END:VCALENDAR\r\n");
|
||||||
|
|
||||||
|
|||||||
@@ -451,22 +451,83 @@ pub async fn create_event(
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse reminders (for now, just store as a simple reminder duration)
|
// Parse reminders and convert to EventReminder structs
|
||||||
let reminders: Vec<chrono::Duration> = match request.reminder.to_lowercase().as_str() {
|
let reminders: Vec<crate::calendar::EventReminder> = match request.reminder.to_lowercase().as_str() {
|
||||||
"15min" => vec![chrono::Duration::minutes(15)],
|
"15min" => vec![crate::calendar::EventReminder {
|
||||||
"30min" => vec![chrono::Duration::minutes(30)],
|
minutes_before: 15,
|
||||||
"1hour" => vec![chrono::Duration::hours(1)],
|
action: crate::calendar::ReminderAction::Display,
|
||||||
"2hours" => vec![chrono::Duration::hours(2)],
|
description: None,
|
||||||
"1day" => vec![chrono::Duration::days(1)],
|
}],
|
||||||
"2days" => vec![chrono::Duration::days(2)],
|
"30min" => vec![crate::calendar::EventReminder {
|
||||||
"1week" => vec![chrono::Duration::weeks(1)],
|
minutes_before: 30,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1hour" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 60,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"2hours" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 120,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1day" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 1440, // 24 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"2days" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 2880, // 48 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1week" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 10080, // 7 * 24 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse recurrence (basic implementation)
|
// Parse recurrence with BYDAY support for weekly recurrence
|
||||||
let recurrence_rule = match request.recurrence.to_lowercase().as_str() {
|
let recurrence_rule = match request.recurrence.to_lowercase().as_str() {
|
||||||
"daily" => Some("FREQ=DAILY".to_string()),
|
"daily" => Some("FREQ=DAILY".to_string()),
|
||||||
"weekly" => Some("FREQ=WEEKLY".to_string()),
|
"weekly" => {
|
||||||
|
// Handle weekly recurrence with optional BYDAY parameter
|
||||||
|
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||||
|
|
||||||
|
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||||
|
if request.recurrence_days.len() == 7 {
|
||||||
|
let selected_days: Vec<&str> = request.recurrence_days
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, &selected)| {
|
||||||
|
if selected {
|
||||||
|
Some(match i {
|
||||||
|
0 => "SU", // Sunday
|
||||||
|
1 => "MO", // Monday
|
||||||
|
2 => "TU", // Tuesday
|
||||||
|
3 => "WE", // Wednesday
|
||||||
|
4 => "TH", // Thursday
|
||||||
|
5 => "FR", // Friday
|
||||||
|
6 => "SA", // Saturday
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !selected_days.is_empty() {
|
||||||
|
rrule.push_str(&format!(";BYDAY={}", selected_days.join(",")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(rrule)
|
||||||
|
},
|
||||||
"monthly" => Some("FREQ=MONTHLY".to_string()),
|
"monthly" => Some("FREQ=MONTHLY".to_string()),
|
||||||
"yearly" => Some("FREQ=YEARLY".to_string()),
|
"yearly" => Some("FREQ=YEARLY".to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ pub struct CreateEventRequest {
|
|||||||
pub categories: String, // comma-separated categories
|
pub categories: String, // comma-separated categories
|
||||||
pub reminder: String, // reminder type
|
pub reminder: String, // reminder type
|
||||||
pub recurrence: String, // recurrence type
|
pub recurrence: String, // recurrence type
|
||||||
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||||
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ pub fn App() -> Html {
|
|||||||
event_data.categories,
|
event_data.categories,
|
||||||
reminder_str,
|
reminder_str,
|
||||||
recurrence_str,
|
recurrence_str,
|
||||||
|
event_data.recurrence_days,
|
||||||
None // Let backend use first available calendar
|
None // Let backend use first available calendar
|
||||||
).await {
|
).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub struct EventCreationData {
|
|||||||
pub categories: String, // Comma-separated list
|
pub categories: String, // Comma-separated list
|
||||||
pub reminder: ReminderType,
|
pub reminder: ReminderType,
|
||||||
pub recurrence: RecurrenceType,
|
pub recurrence: RecurrenceType,
|
||||||
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EventCreationData {
|
impl Default for EventCreationData {
|
||||||
@@ -112,6 +113,7 @@ impl Default for EventCreationData {
|
|||||||
categories: String::new(),
|
categories: String::new(),
|
||||||
reminder: ReminderType::default(),
|
reminder: ReminderType::default(),
|
||||||
recurrence: RecurrenceType::default(),
|
recurrence: RecurrenceType::default(),
|
||||||
|
recurrence_days: vec![false; 7], // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] - all false by default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,11 +292,29 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
|||||||
"yearly" => RecurrenceType::Yearly,
|
"yearly" => RecurrenceType::Yearly,
|
||||||
_ => RecurrenceType::None,
|
_ => RecurrenceType::None,
|
||||||
};
|
};
|
||||||
|
// Reset recurrence days when changing recurrence type
|
||||||
|
data.recurrence_days = vec![false; 7];
|
||||||
event_data.set(data);
|
event_data.set(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_weekday_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
move |day_index: usize| {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
if day_index < data.recurrence_days.len() {
|
||||||
|
data.recurrence_days[day_index] = input.checked();
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let on_start_date_change = {
|
let on_start_date_change = {
|
||||||
let event_data = event_data.clone();
|
let event_data = event_data.clone();
|
||||||
Callback::from(move |e: Event| {
|
Callback::from(move |e: Event| {
|
||||||
@@ -601,6 +621,35 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Show weekday selection only when weekly recurrence is selected
|
||||||
|
if matches!(data.recurrence, RecurrenceType::Weekly) {
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{"Repeat on"}</label>
|
||||||
|
<div class="weekday-selection">
|
||||||
|
{
|
||||||
|
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, day)| {
|
||||||
|
let day_checked = data.recurrence_days.get(i).cloned().unwrap_or(false);
|
||||||
|
let on_change = on_weekday_change(i);
|
||||||
|
html! {
|
||||||
|
<label key={i} class="weekday-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={day_checked}
|
||||||
|
onchange={on_change}
|
||||||
|
/>
|
||||||
|
<span class="weekday-label">{day}</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -607,6 +607,7 @@ impl CalendarService {
|
|||||||
categories: String,
|
categories: String,
|
||||||
reminder: String,
|
reminder: String,
|
||||||
recurrence: String,
|
recurrence: String,
|
||||||
|
recurrence_days: Vec<bool>,
|
||||||
calendar_path: Option<String>
|
calendar_path: Option<String>
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let window = web_sys::window().ok_or("No global window exists")?;
|
let window = web_sys::window().ok_or("No global window exists")?;
|
||||||
@@ -632,6 +633,7 @@ impl CalendarService {
|
|||||||
"categories": categories,
|
"categories": categories,
|
||||||
"reminder": reminder,
|
"reminder": reminder,
|
||||||
"recurrence": recurrence,
|
"recurrence": recurrence,
|
||||||
|
"recurrence_days": recurrence_days,
|
||||||
"calendar_path": calendar_path
|
"calendar_path": calendar_path
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
57
styles.css
57
styles.css
@@ -1271,6 +1271,54 @@ body {
|
|||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Weekday selection styles */
|
||||||
|
.weekday-selection {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
min-width: 3rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox input[type="checkbox"]:checked + .weekday-label {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox:has(input[type="checkbox"]:checked) {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile adjustments for event creation modal */
|
/* Mobile adjustments for event creation modal */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.create-event-modal .form-row {
|
.create-event-modal .form-row {
|
||||||
@@ -1287,4 +1335,13 @@ body {
|
|||||||
.create-event-modal .btn {
|
.create-event-modal .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekday-selection {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox {
|
||||||
|
min-width: 2.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user