- Convert horizontal top navbar to vertical left sidebar - Sidebar features gradient background and fixed positioning - Main content area adjusts with left margin to accommodate sidebar - Mobile responsive: sidebar becomes horizontal top bar on smaller screens - Enhanced navigation styling with hover effects and smooth transitions - Improved space utilization for calendar view 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
13 KiB
Rust
294 lines
13 KiB
Rust
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
use crate::components::{Login, Calendar};
|
|
use crate::services::{CalendarService, CalendarEvent};
|
|
use std::collections::HashMap;
|
|
use chrono::{Local, NaiveDate, Datelike};
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/login")]
|
|
Login,
|
|
#[at("/calendar")]
|
|
Calendar,
|
|
}
|
|
|
|
#[function_component]
|
|
pub fn App() -> Html {
|
|
let auth_token = use_state(|| -> Option<String> {
|
|
LocalStorage::get("auth_token").ok()
|
|
});
|
|
|
|
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();
|
|
Callback::from(move |_| {
|
|
let _ = LocalStorage::delete("auth_token");
|
|
auth_token.set(None);
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<BrowserRouter>
|
|
<div class="app">
|
|
{
|
|
if auth_token.is_some() {
|
|
html! {
|
|
<>
|
|
<aside class="app-sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>{"Calendar App"}</h1>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<Link<Route> to={Route::Calendar} classes="nav-link">{"Calendar"}</Link<Route>>
|
|
<button onclick={on_logout} class="logout-button">{"Logout"}</button>
|
|
</nav>
|
|
</aside>
|
|
<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 /> }
|
|
} else {
|
|
html! { <Redirect<Route> to={Route::Login}/> }
|
|
}
|
|
}
|
|
}
|
|
}} />
|
|
</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 /> }
|
|
} else {
|
|
html! { <Redirect<Route> to={Route::Login}/> }
|
|
}
|
|
}
|
|
}
|
|
}} />
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</BrowserRouter>
|
|
}
|
|
}
|
|
|
|
#[function_component]
|
|
fn CalendarView() -> 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()} />
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {
|
|
<Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} />
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
} |