Added full CalDAV integration to display real calendar events from Baikal server: Backend changes: - Added CalDAV client with iCalendar parsing and XML handling - Created /api/calendar/events endpoint with authentication - Fixed multiline regex patterns for namespace-prefixed XML tags - Added proper calendar path discovery and event filtering Frontend changes: - Created CalendarService for API communication - Updated calendar UI to display events as blue boxes with titles - Added loading states and error handling - Integrated real-time event fetching on calendar load Now successfully displays 3 test events from the Baikal server with proper date formatting and responsive design. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
7.7 KiB
Rust
196 lines
7.7 KiB
Rust
use yew::prelude::*;
|
||
use chrono::{Datelike, Local, NaiveDate, Duration, Weekday};
|
||
use std::collections::HashMap;
|
||
|
||
#[derive(Properties, PartialEq)]
|
||
pub struct CalendarProps {
|
||
#[prop_or_default]
|
||
pub events: HashMap<NaiveDate, Vec<String>>,
|
||
}
|
||
|
||
#[function_component]
|
||
pub fn Calendar(props: &CalendarProps) -> Html {
|
||
let today = Local::now().date_naive();
|
||
let current_month = use_state(|| today);
|
||
|
||
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);
|
||
})
|
||
};
|
||
|
||
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()
|
||
};
|
||
current_month.set(next);
|
||
})
|
||
};
|
||
|
||
html! {
|
||
<div class="calendar">
|
||
<div class="calendar-header">
|
||
<button class="nav-button" onclick={prev_month}>{"‹"}</button>
|
||
<h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2>
|
||
<button class="nav-button" onclick={next_month}>{"›"}</button>
|
||
</div>
|
||
|
||
<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 current month
|
||
{
|
||
(1..=days_in_month).map(|day| {
|
||
let date = current_month.with_day(day).unwrap();
|
||
let is_today = date == today;
|
||
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 !events.is_empty() {
|
||
classes.push("has-events");
|
||
}
|
||
|
||
html! {
|
||
<div class={classes!(classes)}>
|
||
<div class="day-number">{day}</div>
|
||
{
|
||
if !events.is_empty() {
|
||
html! {
|
||
<div class="event-indicators">
|
||
{
|
||
events.iter().take(2).map(|event| {
|
||
html! {
|
||
<div class="event-box" title={event.clone()}>
|
||
{
|
||
if event.len() > 15 {
|
||
format!("{}...", &event[..12])
|
||
} else {
|
||
event.clone()
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
}).collect::<Html>()
|
||
}
|
||
{
|
||
if events.len() > 2 {
|
||
html! { <div class="more-events">{format!("+{} more", events.len() - 2)}</div> }
|
||
} else {
|
||
html! {}
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
} else {
|
||
html! {}
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
}).collect::<Html>()
|
||
}
|
||
|
||
{ render_next_month_days(days_from_prev_month.len(), days_in_month) }
|
||
</div>
|
||
</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 {
|
||
// 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"
|
||
}
|
||
} |