From 9ab6377d168561d07d1b9d9180c28c4b90017e13 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Fri, 29 Aug 2025 10:14:53 -0400 Subject: [PATCH] Refactor calendar component into modular architecture with view switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split monolithic Calendar component into focused sub-components: - CalendarHeader: Navigation buttons and title display - MonthView: Monthly calendar grid layout and event rendering - WeekView: Weekly calendar view with full-height day containers - Add ViewMode enum for Month/Week view switching in sidebar dropdown - Fix event styling by correcting CSS class from "event" to "event-box" - Implement proper week view layout with full-height day containers - Maintain all existing functionality: event handling, context menus, localStorage persistence 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app.rs | 15 +- src/components/calendar.rs | 358 +++++++----------------------- src/components/calendar_header.rs | 54 +++++ src/components/mod.rs | 8 +- src/components/month_view.rs | 183 +++++++++++++++ src/components/route_handler.rs | 11 +- src/components/sidebar.rs | 38 ++++ src/components/week_view.rs | 137 ++++++++++++ styles.css | 59 +++++ 9 files changed, 586 insertions(+), 277 deletions(-) create mode 100644 src/components/calendar_header.rs create mode 100644 src/components/month_view.rs create mode 100644 src/components/week_view.rs diff --git a/src/app.rs b/src/app.rs index ea8a846..e70af75 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ 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, DeleteAction}; +use crate::components::{Sidebar, ViewMode, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; use chrono::NaiveDate; @@ -28,6 +28,9 @@ pub fn App() -> Html { let create_event_modal_open = use_state(|| false); let selected_date_for_event = use_state(|| -> Option { None }); + // Calendar view state + let current_view = use_state(|| ViewMode::Month); + let available_colors = [ "#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#06B6D4", "#84CC16", "#F97316", @@ -52,6 +55,13 @@ pub fn App() -> Html { }) }; + let on_view_change = { + let current_view = current_view.clone(); + Callback::from(move |new_view: ViewMode| { + current_view.set(new_view); + }) + }; + // Fetch user info when token is available { let user_info = user_info.clone(); @@ -354,6 +364,8 @@ pub fn App() -> Html { on_color_picker_toggle={on_color_picker_toggle} available_colors={available_colors.iter().map(|c| c.to_string()).collect::>()} on_calendar_context_menu={on_calendar_context_menu} + current_view={(*current_view).clone()} + on_view_change={on_view_change} />
Html { 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())} + view={(*current_view).clone()} />
diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 1732a38..8f5ee01 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -1,9 +1,9 @@ use yew::prelude::*; -use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; +use chrono::{Datelike, Local, NaiveDate, Duration}; use std::collections::HashMap; +use web_sys::MouseEvent; use crate::services::calendar_service::{CalendarEvent, UserInfo}; -use crate::components::EventModal; -use wasm_bindgen::JsCast; +use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView}; use gloo_storage::{LocalStorage, Storage}; #[derive(Properties, PartialEq)] @@ -19,17 +19,18 @@ pub struct CalendarProps { pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, + #[prop_or_default] + pub view: ViewMode, } #[function_component] pub fn Calendar(props: &CalendarProps) -> Html { let today = Local::now().date_naive(); - let current_month = use_state(|| { - // Try to load saved month from localStorage - if let Ok(saved_month_str) = LocalStorage::get::("calendar_current_month") { - if let Ok(saved_month) = NaiveDate::parse_from_str(&saved_month_str, "%Y-%m-%d") { - // Return the first day of the saved month - saved_month.with_day(1).unwrap_or(today) + let current_date = use_state(|| { + // Try to load saved date from localStorage + if let Ok(saved_date_str) = LocalStorage::get::("calendar_current_month") { + if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { + saved_date.with_day(1).unwrap_or(today) } else { today } @@ -37,217 +38,95 @@ pub fn Calendar(props: &CalendarProps) -> Html { today } }); - let selected_day = use_state(|| today); let selected_event = use_state(|| None::); - // Helper function to get calendar color for an event - let get_event_color = |event: &CalendarEvent| -> String { - if let Some(user_info) = &props.user_info { - if let Some(calendar_path) = &event.calendar_path { - // Find the calendar that matches this event's path - if let Some(calendar) = user_info.calendars.iter() - .find(|cal| &cal.path == calendar_path) { - return calendar.color.clone(); - } - } - } - // Default color if no match found - "#3B82F6".to_string() - }; - - let first_day_of_month = current_month.with_day(1).unwrap(); - let days_in_month = get_days_in_month(*current_month); - let first_weekday = first_day_of_month.weekday(); - let days_from_prev_month = get_days_from_previous_month(*current_month, first_weekday); - - let prev_month = { - let current_month = current_month.clone(); - Callback::from(move |_| { - let prev = *current_month - Duration::days(1); - let first_of_prev = prev.with_day(1).unwrap(); - current_month.set(first_of_prev); - // Save to localStorage - let _ = LocalStorage::set("calendar_current_month", first_of_prev.format("%Y-%m-%d").to_string()); + let on_prev = { + let current_date = current_date.clone(); + let view = props.view.clone(); + Callback::from(move |_: MouseEvent| { + let new_date = match view { + ViewMode::Month => { + let prev = *current_date - Duration::days(1); + prev.with_day(1).unwrap() + }, + ViewMode::Week => *current_date - Duration::weeks(1), + }; + current_date.set(new_date); + let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); }) }; - let next_month = { - let current_month = current_month.clone(); - Callback::from(move |_| { - let next = if current_month.month() == 12 { - NaiveDate::from_ymd_opt(current_month.year() + 1, 1, 1).unwrap() - } else { - NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() + let on_next = { + let current_date = current_date.clone(); + let view = props.view.clone(); + Callback::from(move |_: MouseEvent| { + let new_date = match view { + ViewMode::Month => { + if current_date.month() == 12 { + NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(current_date.year(), current_date.month() + 1, 1).unwrap() + } + }, + ViewMode::Week => *current_date + Duration::weeks(1), }; - current_month.set(next); - // Save to localStorage - let _ = LocalStorage::set("calendar_current_month", next.format("%Y-%m-%d").to_string()); + current_date.set(new_date); + let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); }) }; - let go_to_today = { - let current_month = current_month.clone(); + let on_today = { + let current_date = current_date.clone(); + let view = props.view.clone(); Callback::from(move |_| { let today = Local::now().date_naive(); - let first_of_today_month = today.with_day(1).unwrap(); - current_month.set(first_of_today_month); - // Save to localStorage - let _ = LocalStorage::set("calendar_current_month", first_of_today_month.format("%Y-%m-%d").to_string()); + let new_date = match view { + ViewMode::Month => today.with_day(1).unwrap(), + ViewMode::Week => today, + }; + current_date.set(new_date); + let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); }) }; - + html! { -
-
- -

{format!("{} {}", get_month_name(current_month.month()), current_month.year())}

-
- - -
-
+
Some("week-view"), _ => None })}> + -
- // Weekday headers -
{"Sun"}
-
{"Mon"}
-
{"Tue"}
-
{"Wed"}
-
{"Thu"}
-
{"Fri"}
-
{"Sat"}
- - // Days from previous month (grayed out) - { - days_from_prev_month.iter().map(|day| { - html! { -
{*day}
- } - }).collect::() + { + match props.view { + ViewMode::Month => html! { + + }, + ViewMode::Week => html! { + + }, } - - // Days of current month - { - (1..=days_in_month).map(|day| { - let date = current_month.with_day(day).unwrap(); - let is_today = date == today; - let is_selected = date == *selected_day; - let events = props.events.get(&date).cloned().unwrap_or_default(); - - let mut classes = vec!["calendar-day", "current-month"]; - if is_today { - classes.push("today"); - } - if is_selected { - classes.push("selected"); - } - if !events.is_empty() { - classes.push("has-events"); - } - - let selected_day_clone = selected_day.clone(); - let on_click = Callback::from(move |_| { - selected_day_clone.set(date); - }); - - let on_context_menu = { - let on_calendar_context_menu = props.on_calendar_context_menu.clone(); - Callback::from(move |e: MouseEvent| { - // Only show context menu if we're not right-clicking on an event - if let Some(target) = e.target() { - if let Ok(element) = target.dyn_into::() { - // Check if the click is on an event box or inside one - let mut current = Some(element); - while let Some(el) = current { - if el.class_name().contains("event-box") { - return; // Don't show calendar context menu on events - } - current = el.parent_element(); - } - } - } - - e.prevent_default(); - e.stop_propagation(); - if let Some(callback) = &on_calendar_context_menu { - callback.emit((e, date)); - } - }) - }; - - html! { -
-
{day}
- { - if !events.is_empty() { - html! { -
- { - events.iter().take(2).map(|event| { - let event_clone = event.clone(); - let selected_event_clone = selected_event.clone(); - let on_event_click = props.on_event_click.clone(); - let event_click = Callback::from(move |e: MouseEvent| { - e.stop_propagation(); // Prevent day selection - on_event_click.emit(event_clone.clone()); - selected_event_clone.set(Some(event_clone.clone())); - }); - - let event_context_menu = { - let event_clone = event.clone(); - let on_event_context_menu = props.on_event_context_menu.clone(); - Callback::from(move |e: MouseEvent| { - e.prevent_default(); - e.stop_propagation(); - if let Some(callback) = &on_event_context_menu { - callback.emit((e, event_clone.clone())); - } - }) - }; - - let title = event.get_title(); - let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); - let class_name = if is_refreshing { "event-box refreshing" } else { "event-box" }; - let event_color = get_event_color(&event); - html! { -
- { - if is_refreshing { - "🔄 Refreshing...".to_string() - } else if title.len() > 15 { - format!("{}...", &title[..12]) - } else { - title - } - } -
- } - }).collect::() - } - { - if events.len() > 2 { - html! {
{format!("+{} more", events.len() - 2)}
} - } else { - html! {} - } - } -
- } - } else { - html! {} - } - } -
- } - }).collect::() - } - - { render_next_month_days(days_from_prev_month.len(), days_in_month) } -
+ } // Event details modal Html { />
} -} - -fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html { - let total_slots = 42; // 6 rows x 7 days - let used_slots = prev_days_count + current_days_count as usize; - let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 }; - - (1..=remaining_slots).map(|day| { - html! { -
{day}
- } - }).collect::() -} - -fn get_days_in_month(date: NaiveDate) -> u32 { - NaiveDate::from_ymd_opt( - if date.month() == 12 { date.year() + 1 } else { date.year() }, - if date.month() == 12 { 1 } else { date.month() + 1 }, - 1 - ) - .unwrap() - .pred_opt() - .unwrap() - .day() -} - -fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec { - let days_before = match first_weekday { - Weekday::Sun => 0, - Weekday::Mon => 1, - Weekday::Tue => 2, - Weekday::Wed => 3, - Weekday::Thu => 4, - Weekday::Fri => 5, - Weekday::Sat => 6, - }; - - if days_before == 0 { - vec![] - } else { - // Calculate the previous month - let prev_month = if current_month.month() == 1 { - NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap() - } else { - NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap() - }; - - let prev_month_days = get_days_in_month(prev_month); - ((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect() - } -} - -fn get_month_name(month: u32) -> &'static str { - match month { - 1 => "January", - 2 => "February", - 3 => "March", - 4 => "April", - 5 => "May", - 6 => "June", - 7 => "July", - 8 => "August", - 9 => "September", - 10 => "October", - 11 => "November", - 12 => "December", - _ => "Invalid" - } -} - +} \ No newline at end of file diff --git a/src/components/calendar_header.rs b/src/components/calendar_header.rs new file mode 100644 index 0000000..8d13929 --- /dev/null +++ b/src/components/calendar_header.rs @@ -0,0 +1,54 @@ +use yew::prelude::*; +use chrono::{NaiveDate, Datelike}; +use crate::components::ViewMode; +use web_sys::MouseEvent; + +#[derive(Properties, PartialEq)] +pub struct CalendarHeaderProps { + pub current_date: NaiveDate, + pub view_mode: ViewMode, + pub on_prev: Callback, + pub on_next: Callback, + pub on_today: Callback, +} + +#[function_component(CalendarHeader)] +pub fn calendar_header(props: &CalendarHeaderProps) -> Html { + let title = match props.view_mode { + ViewMode::Month => { + format!("{} {}", get_month_name(props.current_date.month()), props.current_date.year()) + }, + ViewMode::Week => { + format!("Week of {} {}", get_month_name(props.current_date.month()), props.current_date.day()) + }, + }; + + html! { +
+ +

{title}

+
+ + +
+
+ } +} + +fn get_month_name(month: u32) -> &'static str { + match month { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => "Invalid" + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 6777b50..d09ab76 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,5 +1,8 @@ pub mod login; pub mod calendar; +pub mod calendar_header; +pub mod month_view; +pub mod week_view; pub mod event_modal; pub mod create_calendar_modal; pub mod context_menu; @@ -12,12 +15,15 @@ pub mod route_handler; pub use login::Login; pub use calendar::Calendar; +pub use calendar_header::CalendarHeader; +pub use month_view::MonthView; +pub use week_view::WeekView; pub use event_modal::EventModal; pub use create_calendar_modal::CreateCalendarModal; pub use context_menu::ContextMenu; pub use event_context_menu::{EventContextMenu, DeleteAction}; pub use calendar_context_menu::CalendarContextMenu; pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; -pub use sidebar::Sidebar; +pub use sidebar::{Sidebar, ViewMode}; pub use calendar_list_item::CalendarListItem; pub use route_handler::RouteHandler; \ No newline at end of file diff --git a/src/components/month_view.rs b/src/components/month_view.rs new file mode 100644 index 0000000..755a53a --- /dev/null +++ b/src/components/month_view.rs @@ -0,0 +1,183 @@ +use yew::prelude::*; +use chrono::{Datelike, NaiveDate, Weekday}; +use std::collections::HashMap; +use web_sys::MouseEvent; +use crate::services::calendar_service::{CalendarEvent, UserInfo}; + +#[derive(Properties, PartialEq)] +pub struct MonthViewProps { + pub current_month: NaiveDate, + pub today: NaiveDate, + pub events: HashMap>, + pub on_event_click: Callback, + #[prop_or_default] + pub refreshing_event_uid: Option, + #[prop_or_default] + pub user_info: Option, + #[prop_or_default] + pub on_event_context_menu: Option>, + #[prop_or_default] + pub on_calendar_context_menu: Option>, +} + +#[function_component(MonthView)] +pub fn month_view(props: &MonthViewProps) -> Html { + let first_day_of_month = props.current_month.with_day(1).unwrap(); + let days_in_month = get_days_in_month(props.current_month); + let first_weekday = first_day_of_month.weekday(); + let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday); + + // Helper function to get calendar color for an event + let get_event_color = |event: &CalendarEvent| -> String { + if let Some(user_info) = &props.user_info { + if let Some(calendar_path) = &event.calendar_path { + if let Some(calendar) = user_info.calendars.iter() + .find(|cal| &cal.path == calendar_path) { + return calendar.color.clone(); + } + } + } + "#3B82F6".to_string() + }; + + html! { +
+ // Weekday headers +
{"Sun"}
+
{"Mon"}
+
{"Tue"}
+
{"Wed"}
+
{"Thu"}
+
{"Fri"}
+
{"Sat"}
+ + // Days from previous month (grayed out) + { + days_from_prev_month.iter().map(|day| { + html! { +
{*day}
+ } + }).collect::() + } + + // Days of the current month + { + (1..=days_in_month).map(|day| { + let date = props.current_month.with_day(day).unwrap(); + let is_today = date == props.today; + let day_events = props.events.get(&date).cloned().unwrap_or_default(); + + html! { +
+
{day}
+
+ { + day_events.iter().map(|event| { + let event_color = get_event_color(event); + let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); + + let onclick = { + let on_event_click = props.on_event_click.clone(); + let event = event.clone(); + Callback::from(move |_: MouseEvent| { + on_event_click.emit(event.clone()); + }) + }; + + let oncontextmenu = { + if let Some(callback) = &props.on_event_context_menu { + let callback = callback.clone(); + let event = event.clone(); + Some(Callback::from(move |e: web_sys::MouseEvent| { + e.prevent_default(); + callback.emit((e, event.clone())); + })) + } else { + None + } + }; + + html! { +
+ {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} +
+ } + }).collect::() + } +
+
+ } + }).collect::() + } + + { render_next_month_days(days_from_prev_month.len(), days_in_month) } +
+ } +} + +fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html { + let total_slots = 42; // 6 rows x 7 days + let used_slots = prev_days_count + current_days_count as usize; + let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 }; + + (1..=remaining_slots).map(|day| { + html! { +
{day}
+ } + }).collect::() +} + +fn get_days_in_month(date: NaiveDate) -> u32 { + NaiveDate::from_ymd_opt( + if date.month() == 12 { date.year() + 1 } else { date.year() }, + if date.month() == 12 { 1 } else { date.month() + 1 }, + 1 + ) + .unwrap() + .pred_opt() + .unwrap() + .day() +} + +fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec { + let days_before = match first_weekday { + Weekday::Sun => 0, + Weekday::Mon => 1, + Weekday::Tue => 2, + Weekday::Wed => 3, + Weekday::Thu => 4, + Weekday::Fri => 5, + Weekday::Sat => 6, + }; + + if days_before == 0 { + vec![] + } else { + let prev_month = if current_month.month() == 1 { + NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap() + }; + + let prev_month_days = get_days_in_month(prev_month); + ((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect() + } +} \ No newline at end of file diff --git a/src/components/route_handler.rs b/src/components/route_handler.rs index e97e6e2..d8cc943 100644 --- a/src/components/route_handler.rs +++ b/src/components/route_handler.rs @@ -1,6 +1,6 @@ use yew::prelude::*; use yew_router::prelude::*; -use crate::components::Login; +use crate::components::{Login, ViewMode}; use crate::services::calendar_service::{UserInfo, CalendarEvent}; #[derive(Clone, Routable, PartialEq)] @@ -22,6 +22,8 @@ pub struct RouteHandlerProps { pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, + #[prop_or_default] + pub view: ViewMode, } #[function_component(RouteHandler)] @@ -31,6 +33,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { let on_login = props.on_login.clone(); let on_event_context_menu = props.on_event_context_menu.clone(); let on_calendar_context_menu = props.on_calendar_context_menu.clone(); + let view = props.view.clone(); html! { render={move |route| { @@ -39,6 +42,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { let on_login = on_login.clone(); let on_event_context_menu = on_event_context_menu.clone(); let on_calendar_context_menu = on_calendar_context_menu.clone(); + let view = view.clone(); match route { Route::Home => { @@ -62,6 +66,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { user_info={user_info} on_event_context_menu={on_event_context_menu} on_calendar_context_menu={on_calendar_context_menu} + view={view} /> } } else { @@ -80,6 +85,8 @@ pub struct CalendarViewProps { pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, + #[prop_or_default] + pub view: ViewMode, } use gloo_storage::{LocalStorage, Storage}; @@ -238,6 +245,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { user_info={props.user_info.clone()} on_event_context_menu={props.on_event_context_menu.clone()} on_calendar_context_menu={props.on_calendar_context_menu.clone()} + view={props.view.clone()} />
} @@ -250,6 +258,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { user_info={props.user_info.clone()} on_event_context_menu={props.on_event_context_menu.clone()} on_calendar_context_menu={props.on_calendar_context_menu.clone()} + view={props.view.clone()} /> } } diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 8540d84..ab22b74 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,5 +1,6 @@ use yew::prelude::*; use yew_router::prelude::*; +use web_sys::HtmlSelectElement; use crate::services::calendar_service::UserInfo; use crate::components::CalendarListItem; @@ -13,6 +14,18 @@ pub enum Route { Calendar, } +#[derive(Clone, PartialEq)] +pub enum ViewMode { + Month, + Week, +} + +impl Default for ViewMode { + fn default() -> Self { + ViewMode::Month + } +} + #[derive(Properties, PartialEq)] pub struct SidebarProps { pub user_info: Option, @@ -23,10 +36,27 @@ pub struct SidebarProps { pub on_color_picker_toggle: Callback, pub available_colors: Vec, pub on_calendar_context_menu: Callback<(MouseEvent, String)>, + pub current_view: ViewMode, + pub on_view_change: Callback, } #[function_component(Sidebar)] pub fn sidebar(props: &SidebarProps) -> Html { + let on_view_change = { + let on_view_change = props.on_view_change.clone(); + Callback::from(move |e: Event| { + let target = e.target_dyn_into::(); + if let Some(select) = target { + let value = select.value(); + let new_view = match value.as_str() { + "week" => ViewMode::Week, + _ => ViewMode::Month, + }; + on_view_change.emit(new_view); + } + }) + }; + html! { diff --git a/src/components/week_view.rs b/src/components/week_view.rs new file mode 100644 index 0000000..5c71ea2 --- /dev/null +++ b/src/components/week_view.rs @@ -0,0 +1,137 @@ +use yew::prelude::*; +use chrono::{Datelike, NaiveDate, Duration, Weekday}; +use std::collections::HashMap; +use web_sys::MouseEvent; +use crate::services::calendar_service::{CalendarEvent, UserInfo}; + +#[derive(Properties, PartialEq)] +pub struct WeekViewProps { + pub current_date: NaiveDate, + pub today: NaiveDate, + pub events: HashMap>, + pub on_event_click: Callback, + #[prop_or_default] + pub refreshing_event_uid: Option, + #[prop_or_default] + pub user_info: Option, + #[prop_or_default] + pub on_event_context_menu: Option>, + #[prop_or_default] + pub on_calendar_context_menu: Option>, +} + +#[function_component(WeekView)] +pub fn week_view(props: &WeekViewProps) -> Html { + let start_of_week = get_start_of_week(props.current_date); + let week_days: Vec = (0..7) + .map(|i| start_of_week + Duration::days(i)) + .collect(); + + // Helper function to get calendar color for an event + let get_event_color = |event: &CalendarEvent| -> String { + if let Some(user_info) = &props.user_info { + if let Some(calendar_path) = &event.calendar_path { + if let Some(calendar) = user_info.calendars.iter() + .find(|cal| &cal.path == calendar_path) { + return calendar.color.clone(); + } + } + } + "#3B82F6".to_string() + }; + + html! { +
+ // Weekday headers +
{"Sun"}
+
{"Mon"}
+
{"Tue"}
+
{"Wed"}
+
{"Thu"}
+
{"Fri"}
+
{"Sat"}
+ + // Week days + { + week_days.iter().map(|date| { + let is_today = *date == props.today; + let day_events = props.events.get(date).cloned().unwrap_or_default(); + + html! { +
+
{date.day()}
+
+ { + day_events.iter().map(|event| { + let event_color = get_event_color(event); + let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); + + let onclick = { + let on_event_click = props.on_event_click.clone(); + let event = event.clone(); + Callback::from(move |_: MouseEvent| { + on_event_click.emit(event.clone()); + }) + }; + + let oncontextmenu = { + if let Some(callback) = &props.on_event_context_menu { + let callback = callback.clone(); + let event = event.clone(); + Some(Callback::from(move |e: web_sys::MouseEvent| { + e.prevent_default(); + callback.emit((e, event.clone())); + })) + } else { + None + } + }; + + html! { +
+ {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} +
+ } + }).collect::() + } +
+
+ } + }).collect::() + } +
+ } +} + +fn get_start_of_week(date: NaiveDate) -> NaiveDate { + let weekday = date.weekday(); + let days_from_sunday = match weekday { + Weekday::Sun => 0, + Weekday::Mon => 1, + Weekday::Tue => 2, + Weekday::Wed => 3, + Weekday::Thu => 4, + Weekday::Fri => 5, + Weekday::Sat => 6, + }; + date - Duration::days(days_from_sunday) +} \ No newline at end of file diff --git a/styles.css b/styles.css index 99ada49..1a0e6b2 100644 --- a/styles.css +++ b/styles.css @@ -462,6 +462,19 @@ body { background: white; } +/* Week Grid */ +.week-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: auto 1fr; + flex: 1; + background: white; +} + +.week-view .calendar-day { + height: 100%; /* Make week view days stretch to full height of their grid cell */ +} + .weekday-header { background: #f8f9fa; padding: 1rem; @@ -773,6 +786,15 @@ body { border-top: none; } + .view-selector { + margin-bottom: 0.5rem; + } + + .view-selector-dropdown { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + .app-main { margin-left: 0; max-width: 100%; @@ -889,6 +911,43 @@ body { transform: translateY(0); } +/* View Selector */ +.view-selector { + margin-bottom: 1rem; +} + +.view-selector-dropdown { + width: 100%; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 0.75rem 1rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + backdrop-filter: blur(10px); +} + +.view-selector-dropdown:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +.view-selector-dropdown:focus { + outline: none; + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); +} + +.view-selector-dropdown option { + background: #2a2a2a; + color: white; + padding: 0.5rem; +} + /* Create Calendar Modal */ .modal-backdrop { position: fixed;