- 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>
549 lines
27 KiB
Rust
549 lines
27 KiB
Rust
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
use web_sys::MouseEvent;
|
|
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType};
|
|
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
|
use chrono::NaiveDate;
|
|
|
|
|
|
#[function_component]
|
|
pub fn App() -> Html {
|
|
let auth_token = use_state(|| -> Option<String> {
|
|
LocalStorage::get("auth_token").ok()
|
|
});
|
|
|
|
let user_info = use_state(|| -> Option<UserInfo> { None });
|
|
let color_picker_open = use_state(|| -> Option<String> { None });
|
|
let create_modal_open = use_state(|| false);
|
|
let context_menu_open = use_state(|| false);
|
|
let context_menu_pos = use_state(|| (0i32, 0i32));
|
|
let context_menu_calendar_path = use_state(|| -> Option<String> { None });
|
|
let event_context_menu_open = use_state(|| false);
|
|
let event_context_menu_pos = use_state(|| (0i32, 0i32));
|
|
let event_context_menu_event = use_state(|| -> Option<CalendarEvent> { None });
|
|
let calendar_context_menu_open = use_state(|| false);
|
|
let calendar_context_menu_pos = use_state(|| (0i32, 0i32));
|
|
let calendar_context_menu_date = use_state(|| -> Option<NaiveDate> { None });
|
|
let create_event_modal_open = use_state(|| false);
|
|
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
|
|
|
let available_colors = [
|
|
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
|
"#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
|
|
"#EC4899", "#6366F1", "#14B8A6", "#F3B806",
|
|
"#8B5A2B", "#6B7280", "#DC2626", "#7C3AED"
|
|
];
|
|
|
|
let on_login = {
|
|
let auth_token = auth_token.clone();
|
|
Callback::from(move |token: String| {
|
|
auth_token.set(Some(token));
|
|
})
|
|
};
|
|
|
|
let on_logout = {
|
|
let auth_token = auth_token.clone();
|
|
let user_info = user_info.clone();
|
|
Callback::from(move |_| {
|
|
let _ = LocalStorage::delete("auth_token");
|
|
auth_token.set(None);
|
|
user_info.set(None);
|
|
})
|
|
};
|
|
|
|
// Fetch user info when token is available
|
|
{
|
|
let user_info = user_info.clone();
|
|
let auth_token = auth_token.clone();
|
|
|
|
use_effect_with((*auth_token).clone(), move |token| {
|
|
if let Some(token) = token {
|
|
let user_info = user_info.clone();
|
|
let token = token.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
if !password.is_empty() {
|
|
match calendar_service.fetch_user_info(&token, &password).await {
|
|
Ok(mut info) => {
|
|
if let Ok(saved_colors_json) = LocalStorage::get::<String>("calendar_colors") {
|
|
if let Ok(saved_info) = serde_json::from_str::<UserInfo>(&saved_colors_json) {
|
|
for saved_cal in &saved_info.calendars {
|
|
for cal in &mut info.calendars {
|
|
if cal.path == saved_cal.path {
|
|
cal.color = saved_cal.color.clone();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
user_info.set(Some(info));
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("Failed to fetch user info: {}", err).into());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
user_info.set(None);
|
|
}
|
|
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
let on_outside_click = {
|
|
let color_picker_open = color_picker_open.clone();
|
|
let context_menu_open = context_menu_open.clone();
|
|
let event_context_menu_open = event_context_menu_open.clone();
|
|
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
|
Callback::from(move |_: MouseEvent| {
|
|
color_picker_open.set(None);
|
|
context_menu_open.set(false);
|
|
event_context_menu_open.set(false);
|
|
calendar_context_menu_open.set(false);
|
|
})
|
|
};
|
|
|
|
let on_color_change = {
|
|
let user_info = user_info.clone();
|
|
let color_picker_open = color_picker_open.clone();
|
|
Callback::from(move |(calendar_path, color): (String, String)| {
|
|
if let Some(mut info) = (*user_info).clone() {
|
|
for calendar in &mut info.calendars {
|
|
if calendar.path == calendar_path {
|
|
calendar.color = color.clone();
|
|
break;
|
|
}
|
|
}
|
|
user_info.set(Some(info.clone()));
|
|
|
|
if let Ok(json) = serde_json::to_string(&info) {
|
|
let _ = LocalStorage::set("calendar_colors", json);
|
|
}
|
|
}
|
|
color_picker_open.set(None);
|
|
})
|
|
};
|
|
|
|
let on_color_picker_toggle = {
|
|
let color_picker_open = color_picker_open.clone();
|
|
Callback::from(move |calendar_path: String| {
|
|
if color_picker_open.as_ref() == Some(&calendar_path) {
|
|
color_picker_open.set(None);
|
|
} else {
|
|
color_picker_open.set(Some(calendar_path));
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_calendar_context_menu = {
|
|
let context_menu_open = context_menu_open.clone();
|
|
let context_menu_pos = context_menu_pos.clone();
|
|
let context_menu_calendar_path = context_menu_calendar_path.clone();
|
|
Callback::from(move |(event, calendar_path): (MouseEvent, String)| {
|
|
context_menu_open.set(true);
|
|
context_menu_pos.set((event.client_x(), event.client_y()));
|
|
context_menu_calendar_path.set(Some(calendar_path));
|
|
})
|
|
};
|
|
|
|
let on_event_context_menu = {
|
|
let event_context_menu_open = event_context_menu_open.clone();
|
|
let event_context_menu_pos = event_context_menu_pos.clone();
|
|
let event_context_menu_event = event_context_menu_event.clone();
|
|
Callback::from(move |(event, calendar_event): (MouseEvent, CalendarEvent)| {
|
|
event_context_menu_open.set(true);
|
|
event_context_menu_pos.set((event.client_x(), event.client_y()));
|
|
event_context_menu_event.set(Some(calendar_event));
|
|
})
|
|
};
|
|
|
|
let on_calendar_date_context_menu = {
|
|
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
|
let calendar_context_menu_pos = calendar_context_menu_pos.clone();
|
|
let calendar_context_menu_date = calendar_context_menu_date.clone();
|
|
Callback::from(move |(event, date): (MouseEvent, NaiveDate)| {
|
|
calendar_context_menu_open.set(true);
|
|
calendar_context_menu_pos.set((event.client_x(), event.client_y()));
|
|
calendar_context_menu_date.set(Some(date));
|
|
})
|
|
};
|
|
|
|
let on_create_event_click = {
|
|
let create_event_modal_open = create_event_modal_open.clone();
|
|
let selected_date_for_event = selected_date_for_event.clone();
|
|
let calendar_context_menu_date = calendar_context_menu_date.clone();
|
|
Callback::from(move |_: MouseEvent| {
|
|
create_event_modal_open.set(true);
|
|
selected_date_for_event.set((*calendar_context_menu_date).clone());
|
|
})
|
|
};
|
|
|
|
let on_event_create = {
|
|
let create_event_modal_open = create_event_modal_open.clone();
|
|
let auth_token = auth_token.clone();
|
|
Callback::from(move |event_data: EventCreationData| {
|
|
web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into());
|
|
create_event_modal_open.set(false);
|
|
|
|
if let Some(token) = (*auth_token).clone() {
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
// Get CalDAV password from storage
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// Format date and time strings
|
|
let start_date = event_data.start_date.format("%Y-%m-%d").to_string();
|
|
let start_time = event_data.start_time.format("%H:%M").to_string();
|
|
let end_date = event_data.end_date.format("%Y-%m-%d").to_string();
|
|
let end_time = event_data.end_time.format("%H:%M").to_string();
|
|
|
|
// Convert enums to strings for backend
|
|
let status_str = match event_data.status {
|
|
EventStatus::Tentative => "tentative",
|
|
EventStatus::Cancelled => "cancelled",
|
|
_ => "confirmed",
|
|
}.to_string();
|
|
|
|
let class_str = match event_data.class {
|
|
EventClass::Private => "private",
|
|
EventClass::Confidential => "confidential",
|
|
_ => "public",
|
|
}.to_string();
|
|
|
|
let reminder_str = match event_data.reminder {
|
|
ReminderType::Minutes15 => "15min",
|
|
ReminderType::Minutes30 => "30min",
|
|
ReminderType::Hour1 => "1hour",
|
|
ReminderType::Hours2 => "2hours",
|
|
ReminderType::Day1 => "1day",
|
|
ReminderType::Days2 => "2days",
|
|
ReminderType::Week1 => "1week",
|
|
_ => "none",
|
|
}.to_string();
|
|
|
|
let recurrence_str = match event_data.recurrence {
|
|
RecurrenceType::Daily => "daily",
|
|
RecurrenceType::Weekly => "weekly",
|
|
RecurrenceType::Monthly => "monthly",
|
|
RecurrenceType::Yearly => "yearly",
|
|
_ => "none",
|
|
}.to_string();
|
|
|
|
match calendar_service.create_event(
|
|
&token,
|
|
&password,
|
|
event_data.title,
|
|
event_data.description,
|
|
start_date,
|
|
start_time,
|
|
end_date,
|
|
end_time,
|
|
event_data.location,
|
|
event_data.all_day,
|
|
status_str,
|
|
class_str,
|
|
event_data.priority,
|
|
event_data.organizer,
|
|
event_data.attendees,
|
|
event_data.categories,
|
|
reminder_str,
|
|
recurrence_str,
|
|
event_data.recurrence_days,
|
|
None // Let backend use first available calendar
|
|
).await {
|
|
Ok(_) => {
|
|
web_sys::console::log_1(&"Event created successfully".into());
|
|
// Refresh the page to show the new event
|
|
web_sys::window().unwrap().location().reload().unwrap();
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::error_1(&format!("Failed to create event: {}", err).into());
|
|
web_sys::window().unwrap().alert_with_message(&format!("Failed to create event: {}", err)).unwrap();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
})
|
|
};
|
|
|
|
let refresh_calendars = {
|
|
let auth_token = auth_token.clone();
|
|
let user_info = user_info.clone();
|
|
Callback::from(move |_| {
|
|
if let Some(token) = (*auth_token).clone() {
|
|
let user_info = user_info.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
match calendar_service.fetch_user_info(&token, &password).await {
|
|
Ok(mut info) => {
|
|
if let Ok(saved_colors_json) = LocalStorage::get::<String>("calendar_colors") {
|
|
if let Ok(saved_info) = serde_json::from_str::<UserInfo>(&saved_colors_json) {
|
|
for saved_cal in &saved_info.calendars {
|
|
for cal in &mut info.calendars {
|
|
if cal.path == saved_cal.path {
|
|
cal.color = saved_cal.color.clone();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
user_info.set(Some(info));
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("Failed to refresh calendars: {}", err).into());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<BrowserRouter>
|
|
<div class="app" onclick={on_outside_click}>
|
|
{
|
|
if auth_token.is_some() {
|
|
html! {
|
|
<>
|
|
<Sidebar
|
|
user_info={(*user_info).clone()}
|
|
on_logout={on_logout}
|
|
on_create_calendar={Callback::from({
|
|
let create_modal_open = create_modal_open.clone();
|
|
move |_| create_modal_open.set(true)
|
|
})}
|
|
color_picker_open={(*color_picker_open).clone()}
|
|
on_color_change={on_color_change}
|
|
on_color_picker_toggle={on_color_picker_toggle}
|
|
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
|
on_calendar_context_menu={on_calendar_context_menu}
|
|
/>
|
|
<main class="app-main">
|
|
<RouteHandler
|
|
auth_token={(*auth_token).clone()}
|
|
user_info={(*user_info).clone()}
|
|
on_login={on_login.clone()}
|
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
|
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
|
/>
|
|
</main>
|
|
</>
|
|
}
|
|
} else {
|
|
html! {
|
|
<div class="login-layout">
|
|
<RouteHandler
|
|
auth_token={(*auth_token).clone()}
|
|
user_info={(*user_info).clone()}
|
|
on_login={on_login.clone()}
|
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
|
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
|
/>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
<CreateCalendarModal
|
|
is_open={*create_modal_open}
|
|
on_close={Callback::from({
|
|
let create_modal_open = create_modal_open.clone();
|
|
move |_| create_modal_open.set(false)
|
|
})}
|
|
on_create={Callback::from({
|
|
let auth_token = auth_token.clone();
|
|
let refresh_calendars = refresh_calendars.clone();
|
|
let create_modal_open = create_modal_open.clone();
|
|
move |(name, description, color): (String, Option<String>, Option<String>)| {
|
|
if let Some(token) = (*auth_token).clone() {
|
|
let refresh_calendars = refresh_calendars.clone();
|
|
let create_modal_open = create_modal_open.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
match calendar_service.create_calendar(&token, &password, name, description, color).await {
|
|
Ok(_) => {
|
|
web_sys::console::log_1(&"Calendar created successfully!".into());
|
|
refresh_calendars.emit(());
|
|
create_modal_open.set(false);
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into());
|
|
create_modal_open.set(false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
})}
|
|
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
|
/>
|
|
|
|
<ContextMenu
|
|
is_open={*context_menu_open}
|
|
x={context_menu_pos.0}
|
|
y={context_menu_pos.1}
|
|
on_close={Callback::from({
|
|
let context_menu_open = context_menu_open.clone();
|
|
move |_| context_menu_open.set(false)
|
|
})}
|
|
on_delete={Callback::from({
|
|
let auth_token = auth_token.clone();
|
|
let context_menu_calendar_path = context_menu_calendar_path.clone();
|
|
let refresh_calendars = refresh_calendars.clone();
|
|
move |_: MouseEvent| {
|
|
if let (Some(token), Some(calendar_path)) = ((*auth_token).clone(), (*context_menu_calendar_path).clone()) {
|
|
let refresh_calendars = refresh_calendars.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
match calendar_service.delete_calendar(&token, &password, calendar_path).await {
|
|
Ok(_) => {
|
|
web_sys::console::log_1(&"Calendar deleted successfully!".into());
|
|
refresh_calendars.emit(());
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("Failed to delete calendar: {}", err).into());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
})}
|
|
/>
|
|
|
|
<EventContextMenu
|
|
is_open={*event_context_menu_open}
|
|
x={event_context_menu_pos.0}
|
|
y={event_context_menu_pos.1}
|
|
on_close={Callback::from({
|
|
let event_context_menu_open = event_context_menu_open.clone();
|
|
move |_| event_context_menu_open.set(false)
|
|
})}
|
|
on_delete={Callback::from({
|
|
let auth_token = auth_token.clone();
|
|
let event_context_menu_event = event_context_menu_event.clone();
|
|
let event_context_menu_open = event_context_menu_open.clone();
|
|
let refresh_calendars = refresh_calendars.clone();
|
|
move |_: MouseEvent| {
|
|
if let (Some(token), Some(event)) = ((*auth_token).clone(), (*event_context_menu_event).clone()) {
|
|
let _refresh_calendars = refresh_calendars.clone();
|
|
let event_context_menu_open = event_context_menu_open.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
|
credentials["password"].as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) {
|
|
match calendar_service.delete_event(&token, &password, calendar_path.clone(), event_href.clone()).await {
|
|
Ok(_) => {
|
|
web_sys::console::log_1(&"Event deleted successfully!".into());
|
|
// Close the context menu
|
|
event_context_menu_open.set(false);
|
|
// Force a page reload to refresh the calendar events
|
|
web_sys::window().unwrap().location().reload().unwrap();
|
|
}
|
|
Err(err) => {
|
|
web_sys::console::log_1(&format!("Failed to delete event: {}", err).into());
|
|
}
|
|
}
|
|
} else {
|
|
web_sys::console::log_1(&"Missing calendar_path or href - cannot delete event".into());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
})}
|
|
/>
|
|
|
|
<CalendarContextMenu
|
|
is_open={*calendar_context_menu_open}
|
|
x={calendar_context_menu_pos.0}
|
|
y={calendar_context_menu_pos.1}
|
|
on_close={Callback::from({
|
|
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
|
move |_| calendar_context_menu_open.set(false)
|
|
})}
|
|
on_create_event={on_create_event_click}
|
|
/>
|
|
|
|
<CreateEventModal
|
|
is_open={*create_event_modal_open}
|
|
selected_date={(*selected_date_for_event).clone()}
|
|
on_close={Callback::from({
|
|
let create_event_modal_open = create_event_modal_open.clone();
|
|
move |_| create_event_modal_open.set(false)
|
|
})}
|
|
on_create={on_event_create}
|
|
/>
|
|
</div>
|
|
</BrowserRouter>
|
|
}
|
|
} |