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");
|
||||
}
|
||||
|
||||
// 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:VCALENDAR\r\n");
|
||||
|
||||
|
||||
@@ -451,22 +451,83 @@ pub async fn create_event(
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Parse reminders (for now, just store as a simple reminder duration)
|
||||
let reminders: Vec<chrono::Duration> = match request.reminder.to_lowercase().as_str() {
|
||||
"15min" => vec![chrono::Duration::minutes(15)],
|
||||
"30min" => vec![chrono::Duration::minutes(30)],
|
||||
"1hour" => vec![chrono::Duration::hours(1)],
|
||||
"2hours" => vec![chrono::Duration::hours(2)],
|
||||
"1day" => vec![chrono::Duration::days(1)],
|
||||
"2days" => vec![chrono::Duration::days(2)],
|
||||
"1week" => vec![chrono::Duration::weeks(1)],
|
||||
// Parse reminders and convert to EventReminder structs
|
||||
let reminders: Vec<crate::calendar::EventReminder> = match request.reminder.to_lowercase().as_str() {
|
||||
"15min" => vec![crate::calendar::EventReminder {
|
||||
minutes_before: 15,
|
||||
action: crate::calendar::ReminderAction::Display,
|
||||
description: None,
|
||||
}],
|
||||
"30min" => vec![crate::calendar::EventReminder {
|
||||
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(),
|
||||
};
|
||||
|
||||
// Parse recurrence (basic implementation)
|
||||
// Parse recurrence with BYDAY support for weekly recurrence
|
||||
let recurrence_rule = match request.recurrence.to_lowercase().as_str() {
|
||||
"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()),
|
||||
"yearly" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
|
||||
@@ -88,6 +88,7 @@ pub struct CreateEventRequest {
|
||||
pub categories: String, // comma-separated categories
|
||||
pub reminder: String, // reminder 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
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ pub fn App() -> Html {
|
||||
event_data.categories,
|
||||
reminder_str,
|
||||
recurrence_str,
|
||||
event_data.recurrence_days,
|
||||
None // Let backend use first available calendar
|
||||
).await {
|
||||
Ok(_) => {
|
||||
|
||||
@@ -87,6 +87,7 @@ pub struct EventCreationData {
|
||||
pub categories: String, // Comma-separated list
|
||||
pub reminder: ReminderType,
|
||||
pub recurrence: RecurrenceType,
|
||||
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||
}
|
||||
|
||||
impl Default for EventCreationData {
|
||||
@@ -112,6 +113,7 @@ impl Default for EventCreationData {
|
||||
categories: String::new(),
|
||||
reminder: ReminderType::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,
|
||||
_ => RecurrenceType::None,
|
||||
};
|
||||
// Reset recurrence days when changing recurrence type
|
||||
data.recurrence_days = vec![false; 7];
|
||||
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 event_data = event_data.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
@@ -601,6 +621,35 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
||||
</select>
|
||||
</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 class="modal-footer">
|
||||
|
||||
@@ -607,6 +607,7 @@ impl CalendarService {
|
||||
categories: String,
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
calendar_path: Option<String>
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
@@ -632,6 +633,7 @@ impl CalendarService {
|
||||
"categories": categories,
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"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);
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
@media (max-width: 768px) {
|
||||
.create-event-modal .form-row {
|
||||
@@ -1287,4 +1335,13 @@ body {
|
||||
.create-event-modal .btn {
|
||||
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