Compare commits
2 Commits
f9c87369e5
...
b444ae710d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b444ae710d | ||
|
|
c454104c69 |
@@ -658,6 +658,42 @@ impl CalDAVClient {
|
||||
Err(CalDAVError::ServerError(status.as_u16()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a calendar from the CalDAV server
|
||||
pub async fn delete_calendar(&self, calendar_path: &str) -> Result<(), CalDAVError> {
|
||||
let full_url = if calendar_path.starts_with("http") {
|
||||
calendar_path.to_string()
|
||||
} else {
|
||||
// Handle case where calendar_path already contains /dav.php
|
||||
let clean_path = if calendar_path.starts_with("/dav.php") {
|
||||
calendar_path.trim_start_matches("/dav.php")
|
||||
} else {
|
||||
calendar_path
|
||||
};
|
||||
format!("{}{}", self.config.server_url.trim_end_matches('/'), clean_path)
|
||||
};
|
||||
|
||||
println!("Deleting calendar at: {}", full_url);
|
||||
|
||||
let response = self.http_client
|
||||
.delete(&full_url)
|
||||
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| CalDAVError::ParseError(e.to_string()))?;
|
||||
|
||||
println!("Calendar deletion response status: {}", response.status());
|
||||
|
||||
if response.status().is_success() || response.status().as_u16() == 204 {
|
||||
println!("✅ Calendar deleted successfully at {}", calendar_path);
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
println!("❌ Calendar deletion failed: {} - {}", status, error_body);
|
||||
Err(CalDAVError::ServerError(status.as_u16()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct for extracting calendar data from XML responses
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use chrono::Datelike;
|
||||
|
||||
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse}};
|
||||
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse}};
|
||||
use crate::calendar::{CalDAVClient, CalendarEvent};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -292,4 +292,35 @@ pub async fn create_calendar(
|
||||
success: true,
|
||||
message: "Calendar created successfully".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn delete_calendar(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<DeleteCalendarRequest>,
|
||||
) -> Result<Json<DeleteCalendarResponse>, ApiError> {
|
||||
println!("🗑️ Delete calendar request received: path='{}'", request.path);
|
||||
|
||||
// Extract and verify token
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
let password = extract_password_header(&headers)?;
|
||||
|
||||
// Validate request
|
||||
if request.path.trim().is_empty() {
|
||||
return Err(ApiError::BadRequest("Calendar path is required".to_string()));
|
||||
}
|
||||
|
||||
// Create CalDAV config from token and password
|
||||
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
|
||||
let client = CalDAVClient::new(config);
|
||||
|
||||
// Delete the calendar
|
||||
client.delete_calendar(&request.path)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to delete calendar: {}", e)))?;
|
||||
|
||||
Ok(Json(DeleteCalendarResponse {
|
||||
success: true,
|
||||
message: "Calendar deleted successfully".to_string(),
|
||||
}))
|
||||
}
|
||||
@@ -39,6 +39,7 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.route("/api/auth/verify", get(handlers::verify_token))
|
||||
.route("/api/user/info", get(handlers::get_user_info))
|
||||
.route("/api/calendar/create", post(handlers::create_calendar))
|
||||
.route("/api/calendar/delete", post(handlers::delete_calendar))
|
||||
.route("/api/calendar/events", get(handlers::get_calendar_events))
|
||||
.route("/api/calendar/events/:uid", get(handlers::refresh_event))
|
||||
.layer(
|
||||
|
||||
@@ -47,6 +47,17 @@ pub struct CreateCalendarResponse {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeleteCalendarRequest {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeleteCalendarResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// Error handling
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
|
||||
565
src/app.rs
565
src/app.rs
@@ -1,20 +1,10 @@
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use crate::components::{Login, Calendar, CreateCalendarModal};
|
||||
use crate::services::{CalendarService, CalendarEvent, UserInfo};
|
||||
use std::collections::HashMap;
|
||||
use chrono::{Local, NaiveDate, Datelike};
|
||||
use web_sys::MouseEvent;
|
||||
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, RouteHandler};
|
||||
use crate::services::{CalendarService, calendar_service::UserInfo};
|
||||
|
||||
#[derive(Clone, Routable, PartialEq)]
|
||||
enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[at("/calendar")]
|
||||
Calendar,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn App() -> Html {
|
||||
@@ -23,10 +13,12 @@ pub fn App() -> Html {
|
||||
});
|
||||
|
||||
let user_info = use_state(|| -> Option<UserInfo> { None });
|
||||
let color_picker_open = use_state(|| -> Option<String> { None }); // Store calendar path of open picker
|
||||
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 });
|
||||
|
||||
// Available colors for calendar customization
|
||||
let available_colors = [
|
||||
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
||||
"#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
|
||||
@@ -64,7 +56,6 @@ pub fn App() -> Html {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
|
||||
// Get password from stored credentials
|
||||
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()
|
||||
@@ -78,10 +69,8 @@ pub fn App() -> Html {
|
||||
if !password.is_empty() {
|
||||
match calendar_service.fetch_user_info(&token, &password).await {
|
||||
Ok(mut info) => {
|
||||
// Load saved colors from local storage
|
||||
if let Ok(saved_colors_json) = LocalStorage::get::<String>("calendar_colors") {
|
||||
if let Ok(saved_info) = serde_json::from_str::<UserInfo>(&saved_colors_json) {
|
||||
// Update colors with saved preferences
|
||||
for saved_cal in &saved_info.calendars {
|
||||
for cal in &mut info.calendars {
|
||||
if cal.path == saved_cal.path {
|
||||
@@ -109,15 +98,99 @@ pub fn App() -> Html {
|
||||
|
||||
let on_outside_click = {
|
||||
let color_picker_open = color_picker_open.clone();
|
||||
let context_menu_open = context_menu_open.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
color_picker_open.set(None);
|
||||
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);
|
||||
})
|
||||
};
|
||||
|
||||
// Clone variables needed for the modal outside of the conditional blocks
|
||||
let auth_token_for_modal = auth_token.clone();
|
||||
let user_info_for_modal = user_info.clone();
|
||||
let create_modal_open_for_modal = create_modal_open.clone();
|
||||
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 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>
|
||||
@@ -126,187 +199,36 @@ pub fn App() -> Html {
|
||||
if auth_token.is_some() {
|
||||
html! {
|
||||
<>
|
||||
<aside class="app-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>{"Calendar App"}</h1>
|
||||
{
|
||||
if let Some(ref info) = *user_info {
|
||||
html! {
|
||||
<div class="user-info">
|
||||
<div class="username">{&info.username}</div>
|
||||
<div class="server-url">{&info.server_url}</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! { <div class="user-info loading">{"Loading..."}</div> }
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<Link<Route> to={Route::Calendar} classes="nav-link">{"Calendar"}</Link<Route>>
|
||||
</nav>
|
||||
{
|
||||
if let Some(ref info) = *user_info {
|
||||
if !info.calendars.is_empty() {
|
||||
html! {
|
||||
<div class="calendar-list">
|
||||
<h3>{"My Calendars"}</h3>
|
||||
<ul>
|
||||
{
|
||||
info.calendars.iter().map(|cal| {
|
||||
let _cal_clone = cal.clone();
|
||||
let color_picker_open_clone = color_picker_open.clone();
|
||||
|
||||
let on_color_click = {
|
||||
let cal_path = cal.path.clone();
|
||||
let color_picker_open = color_picker_open.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
color_picker_open.set(Some(cal_path.clone()));
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<li key={cal.path.clone()}>
|
||||
<span class="calendar-color"
|
||||
style={format!("background-color: {}", cal.color)}
|
||||
onclick={on_color_click}>
|
||||
{
|
||||
if color_picker_open_clone.as_ref() == Some(&cal.path) {
|
||||
html! {
|
||||
<div class="color-picker">
|
||||
{
|
||||
available_colors.iter().map(|&color| {
|
||||
let color_str = color.to_string();
|
||||
let cal_path = cal.path.clone();
|
||||
let user_info_clone = user_info.clone();
|
||||
let color_picker_open = color_picker_open.clone();
|
||||
|
||||
let on_color_select = Callback::from(move |_: MouseEvent| {
|
||||
// Update the calendar color locally
|
||||
if let Some(mut info) = (*user_info_clone).clone() {
|
||||
for calendar in &mut info.calendars {
|
||||
if calendar.path == cal_path {
|
||||
calendar.color = color_str.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
user_info_clone.set(Some(info.clone()));
|
||||
|
||||
// Save to local storage
|
||||
if let Ok(json) = serde_json::to_string(&info) {
|
||||
let _ = LocalStorage::set("calendar_colors", json);
|
||||
}
|
||||
}
|
||||
color_picker_open.set(None);
|
||||
});
|
||||
|
||||
let is_selected = cal.color == color;
|
||||
let class_name = if is_selected { "color-option selected" } else { "color-option" };
|
||||
|
||||
html! {
|
||||
<div class={class_name}
|
||||
style={format!("background-color: {}", color)}
|
||||
onclick={on_color_select}>
|
||||
</div>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
<span class="calendar-name">{&cal.display_name}</span>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! { <div class="no-calendars">{"No calendars found"}</div> }
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
<div class="sidebar-footer">
|
||||
<button onclick={Callback::from({
|
||||
let create_modal_open = create_modal_open.clone();
|
||||
move |_| create_modal_open.set(true)
|
||||
})} class="create-calendar-button">
|
||||
{"+ Create Calendar"}
|
||||
</button>
|
||||
<button onclick={on_logout} class="logout-button">{"Logout"}</button>
|
||||
</div>
|
||||
</aside>
|
||||
<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">
|
||||
<Switch<Route> render={move |route| {
|
||||
let auth_token = (*auth_token).clone();
|
||||
let on_login = on_login.clone();
|
||||
|
||||
match route {
|
||||
Route::Home => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
Route::Login => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Login {on_login} /> }
|
||||
}
|
||||
}
|
||||
Route::Calendar => {
|
||||
if auth_token.is_some() {
|
||||
html! { <CalendarView user_info={(*user_info).clone()} /> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}} />
|
||||
<RouteHandler
|
||||
auth_token={(*auth_token).clone()}
|
||||
user_info={(*user_info).clone()}
|
||||
on_login={on_login.clone()}
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="login-layout">
|
||||
<Switch<Route> render={move |route| {
|
||||
let auth_token = (*auth_token).clone();
|
||||
let on_login = on_login.clone();
|
||||
|
||||
match route {
|
||||
Route::Home => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
Route::Login => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Login {on_login} /> }
|
||||
}
|
||||
}
|
||||
Route::Calendar => {
|
||||
if auth_token.is_some() {
|
||||
html! { <CalendarView user_info={(*user_info).clone()} /> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}} />
|
||||
<RouteHandler
|
||||
auth_token={(*auth_token).clone()}
|
||||
user_info={(*user_info).clone()}
|
||||
on_login={on_login.clone()}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -315,22 +237,21 @@ pub fn App() -> Html {
|
||||
<CreateCalendarModal
|
||||
is_open={*create_modal_open}
|
||||
on_close={Callback::from({
|
||||
let create_modal_open = create_modal_open_for_modal.clone();
|
||||
let create_modal_open = create_modal_open.clone();
|
||||
move |_| create_modal_open.set(false)
|
||||
})}
|
||||
on_create={Callback::from({
|
||||
let auth_token = auth_token_for_modal.clone();
|
||||
let user_info = user_info_for_modal.clone();
|
||||
let create_modal_open = create_modal_open_for_modal.clone();
|
||||
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 user_info = user_info.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();
|
||||
|
||||
// Get password from stored credentials
|
||||
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()
|
||||
@@ -344,32 +265,11 @@ pub fn App() -> Html {
|
||||
match calendar_service.create_calendar(&token, &password, name, description, color).await {
|
||||
Ok(_) => {
|
||||
web_sys::console::log_1(&"Calendar created successfully!".into());
|
||||
// Refresh user info to show the new calendar
|
||||
match calendar_service.fetch_user_info(&token, &password).await {
|
||||
Ok(mut info) => {
|
||||
// Load saved colors from local storage
|
||||
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());
|
||||
}
|
||||
}
|
||||
refresh_calendars.emit(());
|
||||
create_modal_open.set(false);
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into());
|
||||
// TODO: Show error to user
|
||||
create_modal_open.set(false);
|
||||
}
|
||||
}
|
||||
@@ -379,180 +279,51 @@ pub fn App() -> Html {
|
||||
})}
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CalendarViewProps {
|
||||
pub user_info: Option<UserInfo>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn CalendarView(props: &CalendarViewProps) -> Html {
|
||||
let events = use_state(|| HashMap::<NaiveDate, Vec<CalendarEvent>>::new());
|
||||
let loading = use_state(|| true);
|
||||
let error = use_state(|| None::<String>);
|
||||
let refreshing_event = use_state(|| None::<String>);
|
||||
|
||||
// Get current auth token
|
||||
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
let current_year = today.year();
|
||||
let current_month = today.month();
|
||||
|
||||
// Event refresh callback
|
||||
let on_event_click = {
|
||||
let events = events.clone();
|
||||
let refreshing_event = refreshing_event.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
Callback::from(move |event: CalendarEvent| {
|
||||
if let Some(token) = auth_token.clone() {
|
||||
let events = events.clone();
|
||||
let refreshing_event = refreshing_event.clone();
|
||||
let uid = event.uid.clone();
|
||||
|
||||
refreshing_event.set(Some(uid.clone()));
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
|
||||
// Get password from stored credentials
|
||||
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.refresh_event(&token, &password, &uid).await {
|
||||
Ok(Some(refreshed_event)) => {
|
||||
// If this is a recurring event, we need to regenerate all occurrences
|
||||
let mut updated_events = (*events).clone();
|
||||
|
||||
// First, remove all existing occurrences of this event
|
||||
for (_, day_events) in updated_events.iter_mut() {
|
||||
day_events.retain(|e| e.uid != uid);
|
||||
}
|
||||
|
||||
// Then, if it's a recurring event, generate new occurrences
|
||||
if refreshed_event.recurrence_rule.is_some() {
|
||||
let new_occurrences = CalendarService::expand_recurring_events(vec![refreshed_event.clone()]);
|
||||
|
||||
// Add all new occurrences to the appropriate dates
|
||||
for occurrence in new_occurrences {
|
||||
let date = occurrence.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(occurrence);
|
||||
}
|
||||
} else {
|
||||
// Non-recurring event, just add it to the appropriate date
|
||||
let date = refreshed_event.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(refreshed_event);
|
||||
}
|
||||
|
||||
events.set(updated_events);
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event was deleted, remove it from the map
|
||||
let mut updated_events = (*events).clone();
|
||||
for (_, day_events) in updated_events.iter_mut() {
|
||||
day_events.retain(|e| e.uid != uid);
|
||||
}
|
||||
events.set(updated_events);
|
||||
}
|
||||
Err(_err) => {
|
||||
// Log error but don't show it to user - keep using cached event
|
||||
// Silently fall back to cached event data
|
||||
}
|
||||
}
|
||||
|
||||
refreshing_event.set(None);
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// Fetch events when component mounts
|
||||
{
|
||||
let events = events.clone();
|
||||
let loading = loading.clone();
|
||||
let error = error.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
use_effect_with((), move |_| {
|
||||
if let Some(token) = auth_token {
|
||||
let events = events.clone();
|
||||
let loading = loading.clone();
|
||||
let error = error.clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
|
||||
// Get password from stored credentials
|
||||
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_events_for_month(&token, &password, current_year, current_month).await {
|
||||
Ok(calendar_events) => {
|
||||
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
||||
events.set(grouped_events);
|
||||
loading.set(false);
|
||||
}
|
||||
Err(err) => {
|
||||
error.set(Some(format!("Failed to load events: {}", err)));
|
||||
loading.set(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
loading.set(false);
|
||||
error.set(Some("No authentication token found".to_string()));
|
||||
}
|
||||
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="calendar-view">
|
||||
{
|
||||
if *loading {
|
||||
html! {
|
||||
<div class="calendar-loading">
|
||||
<p>{"Loading calendar events..."}</p>
|
||||
</div>
|
||||
}
|
||||
} else if let Some(err) = (*error).clone() {
|
||||
let dummy_callback = Callback::from(|_: CalendarEvent| {});
|
||||
html! {
|
||||
<div class="calendar-error">
|
||||
<p>{format!("Error: {}", err)}</p>
|
||||
<Calendar events={HashMap::new()} on_event_click={dummy_callback} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
75
src/components/calendar_list_item.rs
Normal file
75
src/components/calendar_list_item.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::MouseEvent;
|
||||
use crate::services::calendar_service::CalendarInfo;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CalendarListItemProps {
|
||||
pub calendar: CalendarInfo,
|
||||
pub color_picker_open: bool,
|
||||
pub on_color_change: Callback<(String, String)>, // (calendar_path, color)
|
||||
pub on_color_picker_toggle: Callback<String>, // calendar_path
|
||||
pub available_colors: Vec<String>,
|
||||
pub on_context_menu: Callback<(MouseEvent, String)>, // (event, calendar_path)
|
||||
}
|
||||
|
||||
#[function_component(CalendarListItem)]
|
||||
pub fn calendar_list_item(props: &CalendarListItemProps) -> Html {
|
||||
let on_color_click = {
|
||||
let cal_path = props.calendar.path.clone();
|
||||
let on_color_picker_toggle = props.on_color_picker_toggle.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
on_color_picker_toggle.emit(cal_path.clone());
|
||||
})
|
||||
};
|
||||
|
||||
let on_context_menu = {
|
||||
let cal_path = props.calendar.path.clone();
|
||||
let on_context_menu = props.on_context_menu.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.prevent_default();
|
||||
on_context_menu.emit((e, cal_path.clone()));
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<li key={props.calendar.path.clone()} oncontextmenu={on_context_menu}>
|
||||
<span class="calendar-color"
|
||||
style={format!("background-color: {}", props.calendar.color)}
|
||||
onclick={on_color_click}>
|
||||
{
|
||||
if props.color_picker_open {
|
||||
html! {
|
||||
<div class="color-picker">
|
||||
{
|
||||
props.available_colors.iter().map(|color| {
|
||||
let color_str = color.clone();
|
||||
let cal_path = props.calendar.path.clone();
|
||||
let on_color_change = props.on_color_change.clone();
|
||||
|
||||
let on_color_select = Callback::from(move |_: MouseEvent| {
|
||||
on_color_change.emit((cal_path.clone(), color_str.clone()));
|
||||
});
|
||||
|
||||
let is_selected = props.calendar.color == *color;
|
||||
let class_name = if is_selected { "color-option selected" } else { "color-option" };
|
||||
|
||||
html! {
|
||||
<div class={class_name}
|
||||
style={format!("background-color: {}", color)}
|
||||
onclick={on_color_select}>
|
||||
</div>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
<span class="calendar-name">{&props.calendar.display_name}</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
49
src/components/context_menu.rs
Normal file
49
src/components/context_menu.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ContextMenuProps {
|
||||
pub is_open: bool,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub on_delete: Callback<MouseEvent>,
|
||||
pub on_close: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component(ContextMenu)]
|
||||
pub fn context_menu(props: &ContextMenuProps) -> Html {
|
||||
let menu_ref = use_node_ref();
|
||||
|
||||
// Close menu when clicking outside (handled by parent component)
|
||||
|
||||
if !props.is_open {
|
||||
return html! {};
|
||||
}
|
||||
|
||||
let style = format!(
|
||||
"position: fixed; left: {}px; top: {}px; z-index: 1001;",
|
||||
props.x, props.y
|
||||
);
|
||||
|
||||
let on_delete_click = {
|
||||
let on_delete = props.on_delete.clone();
|
||||
let on_close = props.on_close.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
on_delete.emit(e);
|
||||
on_close.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div
|
||||
ref={menu_ref}
|
||||
class="context-menu"
|
||||
style={style}
|
||||
>
|
||||
<div class="context-menu-item context-menu-delete" onclick={on_delete_click}>
|
||||
<span class="context-menu-icon">{"🗑️"}</span>
|
||||
{"Delete Calendar"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,16 @@ pub mod login;
|
||||
pub mod calendar;
|
||||
pub mod event_modal;
|
||||
pub mod create_calendar_modal;
|
||||
pub mod context_menu;
|
||||
pub mod sidebar;
|
||||
pub mod calendar_list_item;
|
||||
pub mod route_handler;
|
||||
|
||||
pub use login::Login;
|
||||
pub use calendar::Calendar;
|
||||
pub use event_modal::EventModal;
|
||||
pub use create_calendar_modal::CreateCalendarModal;
|
||||
pub use create_calendar_modal::CreateCalendarModal;
|
||||
pub use context_menu::ContextMenu;
|
||||
pub use sidebar::Sidebar;
|
||||
pub use calendar_list_item::CalendarListItem;
|
||||
pub use route_handler::RouteHandler;
|
||||
226
src/components/route_handler.rs
Normal file
226
src/components/route_handler.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use crate::components::Login;
|
||||
use crate::services::calendar_service::UserInfo;
|
||||
|
||||
#[derive(Clone, Routable, PartialEq)]
|
||||
pub enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[at("/calendar")]
|
||||
Calendar,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct RouteHandlerProps {
|
||||
pub auth_token: Option<String>,
|
||||
pub user_info: Option<UserInfo>,
|
||||
pub on_login: Callback<String>,
|
||||
}
|
||||
|
||||
#[function_component(RouteHandler)]
|
||||
pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
||||
let auth_token = props.auth_token.clone();
|
||||
let user_info = props.user_info.clone();
|
||||
let on_login = props.on_login.clone();
|
||||
|
||||
html! {
|
||||
<Switch<Route> render={move |route| {
|
||||
let auth_token = auth_token.clone();
|
||||
let user_info = user_info.clone();
|
||||
let on_login = on_login.clone();
|
||||
|
||||
match route {
|
||||
Route::Home => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
Route::Login => {
|
||||
if auth_token.is_some() {
|
||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
||||
} else {
|
||||
html! { <Login {on_login} /> }
|
||||
}
|
||||
}
|
||||
Route::Calendar => {
|
||||
if auth_token.is_some() {
|
||||
html! { <CalendarView user_info={user_info} /> }
|
||||
} else {
|
||||
html! { <Redirect<Route> to={Route::Login}/> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}} />
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CalendarViewProps {
|
||||
pub user_info: Option<UserInfo>,
|
||||
}
|
||||
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use crate::services::{CalendarService, CalendarEvent};
|
||||
use crate::components::Calendar;
|
||||
use std::collections::HashMap;
|
||||
use chrono::{Local, NaiveDate, Datelike};
|
||||
|
||||
#[function_component(CalendarView)]
|
||||
pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
||||
let events = use_state(|| HashMap::<NaiveDate, Vec<CalendarEvent>>::new());
|
||||
let loading = use_state(|| true);
|
||||
let error = use_state(|| None::<String>);
|
||||
let refreshing_event = use_state(|| None::<String>);
|
||||
|
||||
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
let current_year = today.year();
|
||||
let current_month = today.month();
|
||||
|
||||
let on_event_click = {
|
||||
let events = events.clone();
|
||||
let refreshing_event = refreshing_event.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
Callback::from(move |event: CalendarEvent| {
|
||||
if let Some(token) = auth_token.clone() {
|
||||
let events = events.clone();
|
||||
let refreshing_event = refreshing_event.clone();
|
||||
let uid = event.uid.clone();
|
||||
|
||||
refreshing_event.set(Some(uid.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.refresh_event(&token, &password, &uid).await {
|
||||
Ok(Some(refreshed_event)) => {
|
||||
let mut updated_events = (*events).clone();
|
||||
|
||||
for (_, day_events) in updated_events.iter_mut() {
|
||||
day_events.retain(|e| e.uid != uid);
|
||||
}
|
||||
|
||||
if refreshed_event.recurrence_rule.is_some() {
|
||||
let new_occurrences = CalendarService::expand_recurring_events(vec![refreshed_event.clone()]);
|
||||
|
||||
for occurrence in new_occurrences {
|
||||
let date = occurrence.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(occurrence);
|
||||
}
|
||||
} else {
|
||||
let date = refreshed_event.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(refreshed_event);
|
||||
}
|
||||
|
||||
events.set(updated_events);
|
||||
}
|
||||
Ok(None) => {
|
||||
let mut updated_events = (*events).clone();
|
||||
for (_, day_events) in updated_events.iter_mut() {
|
||||
day_events.retain(|e| e.uid != uid);
|
||||
}
|
||||
events.set(updated_events);
|
||||
}
|
||||
Err(_err) => {
|
||||
}
|
||||
}
|
||||
|
||||
refreshing_event.set(None);
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
let events = events.clone();
|
||||
let loading = loading.clone();
|
||||
let error = error.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
use_effect_with((), move |_| {
|
||||
if let Some(token) = auth_token {
|
||||
let events = events.clone();
|
||||
let loading = loading.clone();
|
||||
let error = error.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_events_for_month(&token, &password, current_year, current_month).await {
|
||||
Ok(calendar_events) => {
|
||||
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
||||
events.set(grouped_events);
|
||||
loading.set(false);
|
||||
}
|
||||
Err(err) => {
|
||||
error.set(Some(format!("Failed to load events: {}", err)));
|
||||
loading.set(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
loading.set(false);
|
||||
error.set(Some("No authentication token found".to_string()));
|
||||
}
|
||||
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="calendar-view">
|
||||
{
|
||||
if *loading {
|
||||
html! {
|
||||
<div class="calendar-loading">
|
||||
<p>{"Loading calendar events..."}</p>
|
||||
</div>
|
||||
}
|
||||
} else if let Some(err) = (*error).clone() {
|
||||
let dummy_callback = Callback::from(|_: CalendarEvent| {});
|
||||
html! {
|
||||
<div class="calendar-error">
|
||||
<p>{format!("Error: {}", err)}</p>
|
||||
<Calendar events={HashMap::new()} on_event_click={dummy_callback} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
89
src/components/sidebar.rs
Normal file
89
src/components/sidebar.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use crate::services::calendar_service::UserInfo;
|
||||
use crate::components::CalendarListItem;
|
||||
|
||||
#[derive(Clone, Routable, PartialEq)]
|
||||
pub enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[at("/calendar")]
|
||||
Calendar,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct SidebarProps {
|
||||
pub user_info: Option<UserInfo>,
|
||||
pub on_logout: Callback<()>,
|
||||
pub on_create_calendar: Callback<()>,
|
||||
pub color_picker_open: Option<String>,
|
||||
pub on_color_change: Callback<(String, String)>,
|
||||
pub on_color_picker_toggle: Callback<String>,
|
||||
pub available_colors: Vec<String>,
|
||||
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
|
||||
}
|
||||
|
||||
#[function_component(Sidebar)]
|
||||
pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
html! {
|
||||
<aside class="app-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>{"Calendar App"}</h1>
|
||||
{
|
||||
if let Some(ref info) = props.user_info {
|
||||
html! {
|
||||
<div class="user-info">
|
||||
<div class="username">{&info.username}</div>
|
||||
<div class="server-url">{&info.server_url}</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! { <div class="user-info loading">{"Loading..."}</div> }
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<Link<Route> to={Route::Calendar} classes="nav-link">{"Calendar"}</Link<Route>>
|
||||
</nav>
|
||||
{
|
||||
if let Some(ref info) = props.user_info {
|
||||
if !info.calendars.is_empty() {
|
||||
html! {
|
||||
<div class="calendar-list">
|
||||
<h3>{"My Calendars"}</h3>
|
||||
<ul>
|
||||
{
|
||||
info.calendars.iter().map(|cal| {
|
||||
html! {
|
||||
<CalendarListItem
|
||||
calendar={cal.clone()}
|
||||
color_picker_open={props.color_picker_open.as_ref() == Some(&cal.path)}
|
||||
on_color_change={props.on_color_change.clone()}
|
||||
on_color_picker_toggle={props.on_color_picker_toggle.clone()}
|
||||
available_colors={props.available_colors.clone()}
|
||||
on_context_menu={props.on_calendar_context_menu.clone()}
|
||||
/>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! { <div class="no-calendars">{"No calendars found"}</div> }
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
<div class="sidebar-footer">
|
||||
<button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button">
|
||||
{"+ Create Calendar"}
|
||||
</button>
|
||||
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
@@ -528,6 +528,63 @@ impl CalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a calendar from the CalDAV server
|
||||
pub async fn delete_calendar(
|
||||
&self,
|
||||
token: &str,
|
||||
password: &str,
|
||||
path: String
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
let opts = RequestInit::new();
|
||||
opts.set_method("POST");
|
||||
opts.set_mode(RequestMode::Cors);
|
||||
|
||||
let body = serde_json::json!({
|
||||
"path": path
|
||||
});
|
||||
|
||||
let body_string = serde_json::to_string(&body)
|
||||
.map_err(|e| format!("JSON serialization failed: {}", e))?;
|
||||
|
||||
opts.set_body(&body_string.into());
|
||||
|
||||
let url = format!("{}/calendar/delete", self.base_url);
|
||||
let request = Request::new_with_str_and_init(&url, &opts)
|
||||
.map_err(|e| format!("Request creation failed: {:?}", e))?;
|
||||
|
||||
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||
|
||||
request.headers().set("X-CalDAV-Password", password)
|
||||
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||
|
||||
request.headers().set("Content-Type", "application/json")
|
||||
.map_err(|e| format!("Content-Type header setting failed: {:?}", e))?;
|
||||
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
.map_err(|e| format!("Network request failed: {:?}", e))?;
|
||||
|
||||
let resp: Response = resp_value.dyn_into()
|
||||
.map_err(|e| format!("Response cast failed: {:?}", e))?;
|
||||
|
||||
let text = JsFuture::from(resp.text()
|
||||
.map_err(|e| format!("Text extraction failed: {:?}", e))?)
|
||||
.await
|
||||
.map_err(|e| format!("Text promise failed: {:?}", e))?;
|
||||
|
||||
let text_string = text.as_string()
|
||||
.ok_or("Response text is not a string")?;
|
||||
|
||||
if resp.ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Request failed with status {}: {}", resp.status(), text_string))
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh a single event by UID from the CalDAV server
|
||||
pub async fn refresh_event(&self, token: &str, password: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod calendar_service;
|
||||
|
||||
pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction, UserInfo};
|
||||
pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction};
|
||||
62
styles.css
62
styles.css
@@ -1107,4 +1107,66 @@ body {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
.context-menu {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
animation: contextMenuSlideIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes contextMenuSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.context-menu-delete {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.context-menu-delete:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Prevent text selection on context menu items */
|
||||
.context-menu-item {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
Reference in New Issue
Block a user