Files
calendar/src/app.rs
Connor Johnstone 8a0d2286dc Replace top navbar with left sidebar navigation
- 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>
2025-08-28 19:46:43 -04:00

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>
}
}