Some checks failed
Build and Push Docker Image / docker (push) Failing after 1m7s
Moved event fetching logic from CalendarView to Calendar component to properly use the visible date range instead of hardcoded current month. The Calendar component already tracks the current visible date through navigation, so events now load correctly for August and other months when navigating. Changes: - Calendar component now manages its own events state and fetching - Event fetching responds to current_date changes from navigation - CalendarView simplified to just render Calendar component - Fixed cargo fmt/clippy formatting across codebase 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
289 lines
12 KiB
Rust
289 lines
12 KiB
Rust
use crate::models::ical::VEvent;
|
|
use crate::services::calendar_service::UserInfo;
|
|
use chrono::{Datelike, NaiveDate, Weekday};
|
|
use std::collections::HashMap;
|
|
use wasm_bindgen::{prelude::*, JsCast};
|
|
use web_sys::window;
|
|
use yew::prelude::*;
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct MonthViewProps {
|
|
pub current_month: NaiveDate,
|
|
pub today: NaiveDate,
|
|
pub events: HashMap<NaiveDate, Vec<VEvent>>,
|
|
pub on_event_click: Callback<VEvent>,
|
|
#[prop_or_default]
|
|
pub refreshing_event_uid: Option<String>,
|
|
#[prop_or_default]
|
|
pub user_info: Option<UserInfo>,
|
|
#[prop_or_default]
|
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, VEvent)>>,
|
|
#[prop_or_default]
|
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
|
#[prop_or_default]
|
|
pub selected_date: Option<NaiveDate>,
|
|
#[prop_or_default]
|
|
pub on_day_select: Option<Callback<NaiveDate>>,
|
|
}
|
|
|
|
#[function_component(MonthView)]
|
|
pub fn month_view(props: &MonthViewProps) -> Html {
|
|
let max_events_per_day = use_state(|| 4); // Default to 4 events max
|
|
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);
|
|
|
|
// Calculate maximum events that can fit based on available height
|
|
let calculate_max_events = {
|
|
let max_events_per_day = max_events_per_day.clone();
|
|
move || {
|
|
// Since we're using CSS Grid with equal row heights,
|
|
// we can estimate based on typical calendar dimensions
|
|
// Typical calendar height is around 600-800px for 6 rows
|
|
// Each row gets ~100-133px, minus day number and padding leaves ~70-100px
|
|
// Each event is ~18px, so we can fit ~3-4 events + "+n more" indicator
|
|
max_events_per_day.set(3);
|
|
}
|
|
};
|
|
|
|
// Setup resize handler and initial calculation
|
|
{
|
|
let calculate_max_events = calculate_max_events.clone();
|
|
use_effect_with((), move |_| {
|
|
let calculate_max_events_clone = calculate_max_events.clone();
|
|
|
|
// Initial calculation with a slight delay to ensure DOM is ready
|
|
if let Some(window) = window() {
|
|
let timeout_closure = Closure::wrap(Box::new(move || {
|
|
calculate_max_events_clone();
|
|
}) as Box<dyn FnMut()>);
|
|
|
|
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
|
timeout_closure.as_ref().unchecked_ref(),
|
|
100,
|
|
);
|
|
timeout_closure.forget();
|
|
}
|
|
|
|
// Setup resize listener
|
|
let resize_closure = Closure::wrap(Box::new(move || {
|
|
calculate_max_events();
|
|
}) as Box<dyn Fn()>);
|
|
|
|
if let Some(window) = window() {
|
|
let _ = window.add_event_listener_with_callback(
|
|
"resize",
|
|
resize_closure.as_ref().unchecked_ref(),
|
|
);
|
|
resize_closure.forget(); // Keep the closure alive
|
|
}
|
|
|
|
|| {}
|
|
});
|
|
}
|
|
|
|
// Helper function to get calendar color for an event
|
|
let get_event_color = |event: &VEvent| -> 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! {
|
|
<div class="calendar-grid">
|
|
// Weekday headers
|
|
<div class="weekday-header">{"Sun"}</div>
|
|
<div class="weekday-header">{"Mon"}</div>
|
|
<div class="weekday-header">{"Tue"}</div>
|
|
<div class="weekday-header">{"Wed"}</div>
|
|
<div class="weekday-header">{"Thu"}</div>
|
|
<div class="weekday-header">{"Fri"}</div>
|
|
<div class="weekday-header">{"Sat"}</div>
|
|
|
|
// Days from previous month (grayed out)
|
|
{
|
|
days_from_prev_month.iter().map(|day| {
|
|
html! {
|
|
<div class="calendar-day prev-month">{*day}</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
|
|
// 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 is_selected = props.selected_date == Some(date);
|
|
let day_events = props.events.get(&date).cloned().unwrap_or_default();
|
|
|
|
// Calculate visible events and overflow
|
|
let max_events = *max_events_per_day as usize;
|
|
let visible_events: Vec<_> = day_events.iter().take(max_events).collect();
|
|
let hidden_count = day_events.len().saturating_sub(max_events);
|
|
|
|
html! {
|
|
<div
|
|
class={classes!(
|
|
"calendar-day",
|
|
if is_today { Some("today") } else { None },
|
|
if is_selected { Some("selected") } else { None }
|
|
)}
|
|
onclick={
|
|
if let Some(callback) = &props.on_day_select {
|
|
let callback = callback.clone();
|
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.stop_propagation(); // Prevent other handlers
|
|
callback.emit(date);
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
oncontextmenu={
|
|
if let Some(callback) = &props.on_calendar_context_menu {
|
|
let callback = callback.clone();
|
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
callback.emit((e, date));
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
>
|
|
<div class="day-number">{day}</div>
|
|
<div class="day-events">
|
|
{
|
|
visible_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 |_: web_sys::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! {
|
|
<div
|
|
class={classes!("event-box", if is_refreshing { Some("refreshing") } else { None })}
|
|
style={format!("background-color: {}", event_color)}
|
|
{onclick}
|
|
{oncontextmenu}
|
|
>
|
|
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
{
|
|
if hidden_count > 0 {
|
|
html! {
|
|
<div class="more-events-indicator">
|
|
{format!("+{} more", hidden_count)}
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
|
|
{ render_next_month_days(days_from_prev_month.len(), days_in_month) }
|
|
</div>
|
|
}
|
|
}
|
|
|
|
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! {
|
|
<div class="calendar-day next-month">{day}</div>
|
|
}
|
|
})
|
|
.collect::<Html>()
|
|
}
|
|
|
|
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<u32> {
|
|
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()
|
|
}
|
|
}
|