Refactor calendar component into modular architecture with view switching
- 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 <noreply@anthropic.com>
This commit is contained in:
15
src/app.rs
15
src/app.rs
@@ -2,7 +2,7 @@ use yew::prelude::*;
|
|||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use web_sys::MouseEvent;
|
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 crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ pub fn App() -> Html {
|
|||||||
let create_event_modal_open = use_state(|| false);
|
let create_event_modal_open = use_state(|| false);
|
||||||
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
||||||
|
|
||||||
|
// Calendar view state
|
||||||
|
let current_view = use_state(|| ViewMode::Month);
|
||||||
|
|
||||||
let available_colors = [
|
let available_colors = [
|
||||||
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
||||||
"#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
|
"#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
|
// Fetch user info when token is available
|
||||||
{
|
{
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
@@ -354,6 +364,8 @@ pub fn App() -> Html {
|
|||||||
on_color_picker_toggle={on_color_picker_toggle}
|
on_color_picker_toggle={on_color_picker_toggle}
|
||||||
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
||||||
on_calendar_context_menu={on_calendar_context_menu}
|
on_calendar_context_menu={on_calendar_context_menu}
|
||||||
|
current_view={(*current_view).clone()}
|
||||||
|
on_view_change={on_view_change}
|
||||||
/>
|
/>
|
||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
<RouteHandler
|
<RouteHandler
|
||||||
@@ -362,6 +374,7 @@ pub fn App() -> Html {
|
|||||||
on_login={on_login.clone()}
|
on_login={on_login.clone()}
|
||||||
on_event_context_menu={Some(on_event_context_menu.clone())}
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||||||
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
||||||
|
view={(*current_view).clone()}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use chrono::{Datelike, Local, NaiveDate, Duration, Weekday};
|
use chrono::{Datelike, Local, NaiveDate, Duration};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
||||||
use crate::components::EventModal;
|
use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView};
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
@@ -19,17 +19,18 @@ pub struct CalendarProps {
|
|||||||
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub view: ViewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Calendar(props: &CalendarProps) -> Html {
|
pub fn Calendar(props: &CalendarProps) -> Html {
|
||||||
let today = Local::now().date_naive();
|
let today = Local::now().date_naive();
|
||||||
let current_month = use_state(|| {
|
let current_date = use_state(|| {
|
||||||
// Try to load saved month from localStorage
|
// Try to load saved date from localStorage
|
||||||
if let Ok(saved_month_str) = LocalStorage::get::<String>("calendar_current_month") {
|
if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_current_month") {
|
||||||
if let Ok(saved_month) = NaiveDate::parse_from_str(&saved_month_str, "%Y-%m-%d") {
|
if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") {
|
||||||
// Return the first day of the saved month
|
saved_date.with_day(1).unwrap_or(today)
|
||||||
saved_month.with_day(1).unwrap_or(today)
|
|
||||||
} else {
|
} else {
|
||||||
today
|
today
|
||||||
}
|
}
|
||||||
@@ -37,217 +38,95 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
today
|
today
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let selected_day = use_state(|| today);
|
|
||||||
let selected_event = use_state(|| None::<CalendarEvent>);
|
let selected_event = use_state(|| None::<CalendarEvent>);
|
||||||
|
|
||||||
// Helper function to get calendar color for an event
|
let on_prev = {
|
||||||
let get_event_color = |event: &CalendarEvent| -> String {
|
let current_date = current_date.clone();
|
||||||
if let Some(user_info) = &props.user_info {
|
let view = props.view.clone();
|
||||||
if let Some(calendar_path) = &event.calendar_path {
|
Callback::from(move |_: MouseEvent| {
|
||||||
// Find the calendar that matches this event's path
|
let new_date = match view {
|
||||||
if let Some(calendar) = user_info.calendars.iter()
|
ViewMode::Month => {
|
||||||
.find(|cal| &cal.path == calendar_path) {
|
let prev = *current_date - Duration::days(1);
|
||||||
return calendar.color.clone();
|
prev.with_day(1).unwrap()
|
||||||
}
|
},
|
||||||
}
|
ViewMode::Week => *current_date - Duration::weeks(1),
|
||||||
}
|
};
|
||||||
// Default color if no match found
|
current_date.set(new_date);
|
||||||
"#3B82F6".to_string()
|
let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").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 next_month = {
|
let on_next = {
|
||||||
let current_month = current_month.clone();
|
let current_date = current_date.clone();
|
||||||
Callback::from(move |_| {
|
let view = props.view.clone();
|
||||||
let next = if current_month.month() == 12 {
|
Callback::from(move |_: MouseEvent| {
|
||||||
NaiveDate::from_ymd_opt(current_month.year() + 1, 1, 1).unwrap()
|
let new_date = match view {
|
||||||
} else {
|
ViewMode::Month => {
|
||||||
NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap()
|
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);
|
current_date.set(new_date);
|
||||||
// Save to localStorage
|
let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string());
|
||||||
let _ = LocalStorage::set("calendar_current_month", next.format("%Y-%m-%d").to_string());
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let go_to_today = {
|
let on_today = {
|
||||||
let current_month = current_month.clone();
|
let current_date = current_date.clone();
|
||||||
|
let view = props.view.clone();
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_| {
|
||||||
let today = Local::now().date_naive();
|
let today = Local::now().date_naive();
|
||||||
let first_of_today_month = today.with_day(1).unwrap();
|
let new_date = match view {
|
||||||
current_month.set(first_of_today_month);
|
ViewMode::Month => today.with_day(1).unwrap(),
|
||||||
// Save to localStorage
|
ViewMode::Week => today,
|
||||||
let _ = LocalStorage::set("calendar_current_month", first_of_today_month.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());
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="calendar">
|
<div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}>
|
||||||
<div class="calendar-header">
|
<CalendarHeader
|
||||||
<button class="nav-button" onclick={prev_month}>{"‹"}</button>
|
current_date={*current_date}
|
||||||
<h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2>
|
view_mode={props.view.clone()}
|
||||||
<div class="header-right">
|
on_prev={on_prev}
|
||||||
<button class="today-button" onclick={go_to_today}>{"Today"}</button>
|
on_next={on_next}
|
||||||
<button class="nav-button" onclick={next_month}>{"›"}</button>
|
on_today={on_today}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="calendar-grid">
|
{
|
||||||
// Weekday headers
|
match props.view {
|
||||||
<div class="weekday-header">{"Sun"}</div>
|
ViewMode::Month => html! {
|
||||||
<div class="weekday-header">{"Mon"}</div>
|
<MonthView
|
||||||
<div class="weekday-header">{"Tue"}</div>
|
current_month={*current_date}
|
||||||
<div class="weekday-header">{"Wed"}</div>
|
today={today}
|
||||||
<div class="weekday-header">{"Thu"}</div>
|
events={props.events.clone()}
|
||||||
<div class="weekday-header">{"Fri"}</div>
|
on_event_click={props.on_event_click.clone()}
|
||||||
<div class="weekday-header">{"Sat"}</div>
|
refreshing_event_uid={props.refreshing_event_uid.clone()}
|
||||||
|
user_info={props.user_info.clone()}
|
||||||
// Days from previous month (grayed out)
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
{
|
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
days_from_prev_month.iter().map(|day| {
|
/>
|
||||||
html! {
|
},
|
||||||
<div class="calendar-day prev-month">{*day}</div>
|
ViewMode::Week => html! {
|
||||||
}
|
<WeekView
|
||||||
}).collect::<Html>()
|
current_date={*current_date}
|
||||||
|
today={today}
|
||||||
|
events={props.events.clone()}
|
||||||
|
on_event_click={props.on_event_click.clone()}
|
||||||
|
refreshing_event_uid={props.refreshing_event_uid.clone()}
|
||||||
|
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()}
|
||||||
|
/>
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 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::<web_sys::Element>() {
|
|
||||||
// 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! {
|
|
||||||
<div class={classes!(classes)} onclick={on_click} oncontextmenu={on_context_menu}>
|
|
||||||
<div class="day-number">{day}</div>
|
|
||||||
{
|
|
||||||
if !events.is_empty() {
|
|
||||||
html! {
|
|
||||||
<div class="event-indicators">
|
|
||||||
{
|
|
||||||
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! {
|
|
||||||
<div class={class_name}
|
|
||||||
title={title.clone()}
|
|
||||||
onclick={event_click}
|
|
||||||
oncontextmenu={event_context_menu}
|
|
||||||
style={format!("background-color: {}", event_color)}>
|
|
||||||
{
|
|
||||||
if is_refreshing {
|
|
||||||
"🔄 Refreshing...".to_string()
|
|
||||||
} else if title.len() > 15 {
|
|
||||||
format!("{}...", &title[..12])
|
|
||||||
} else {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
// Event details modal
|
// Event details modal
|
||||||
<EventModal
|
<EventModal
|
||||||
@@ -261,73 +140,4 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
/>
|
/>
|
||||||
</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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
54
src/components/calendar_header.rs
Normal file
54
src/components/calendar_header.rs
Normal file
@@ -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<MouseEvent>,
|
||||||
|
pub on_next: Callback<MouseEvent>,
|
||||||
|
pub on_today: Callback<MouseEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button>
|
||||||
|
<h2 class="month-year">{title}</h2>
|
||||||
|
<div class="header-right">
|
||||||
|
<button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button>
|
||||||
|
<button class="nav-button" onclick={props.on_next.clone()}>{"›"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
|
pub mod calendar_header;
|
||||||
|
pub mod month_view;
|
||||||
|
pub mod week_view;
|
||||||
pub mod event_modal;
|
pub mod event_modal;
|
||||||
pub mod create_calendar_modal;
|
pub mod create_calendar_modal;
|
||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
@@ -12,12 +15,15 @@ pub mod route_handler;
|
|||||||
|
|
||||||
pub use login::Login;
|
pub use login::Login;
|
||||||
pub use calendar::Calendar;
|
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 event_modal::EventModal;
|
||||||
pub use create_calendar_modal::CreateCalendarModal;
|
pub use create_calendar_modal::CreateCalendarModal;
|
||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
pub use event_context_menu::{EventContextMenu, DeleteAction};
|
pub use event_context_menu::{EventContextMenu, DeleteAction};
|
||||||
pub use calendar_context_menu::CalendarContextMenu;
|
pub use calendar_context_menu::CalendarContextMenu;
|
||||||
pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
|
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 calendar_list_item::CalendarListItem;
|
||||||
pub use route_handler::RouteHandler;
|
pub use route_handler::RouteHandler;
|
||||||
183
src/components/month_view.rs
Normal file
183
src/components/month_view.rs
Normal file
@@ -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<NaiveDate, Vec<CalendarEvent>>,
|
||||||
|
pub on_event_click: Callback<CalendarEvent>,
|
||||||
|
#[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, CalendarEvent)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
<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 day_events = props.events.get(&date).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
class={classes!("calendar-day", if is_today { Some("today") } 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">
|
||||||
|
{
|
||||||
|
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! {
|
||||||
|
<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>()
|
||||||
|
}
|
||||||
|
</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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use crate::components::Login;
|
use crate::components::{Login, ViewMode};
|
||||||
use crate::services::calendar_service::{UserInfo, CalendarEvent};
|
use crate::services::calendar_service::{UserInfo, CalendarEvent};
|
||||||
|
|
||||||
#[derive(Clone, Routable, PartialEq)]
|
#[derive(Clone, Routable, PartialEq)]
|
||||||
@@ -22,6 +22,8 @@ pub struct RouteHandlerProps {
|
|||||||
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub view: ViewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(RouteHandler)]
|
#[function_component(RouteHandler)]
|
||||||
@@ -31,6 +33,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
let on_login = props.on_login.clone();
|
let on_login = props.on_login.clone();
|
||||||
let on_event_context_menu = props.on_event_context_menu.clone();
|
let on_event_context_menu = props.on_event_context_menu.clone();
|
||||||
let on_calendar_context_menu = props.on_calendar_context_menu.clone();
|
let on_calendar_context_menu = props.on_calendar_context_menu.clone();
|
||||||
|
let view = props.view.clone();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Route> render={move |route| {
|
<Switch<Route> render={move |route| {
|
||||||
@@ -39,6 +42,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
let on_login = on_login.clone();
|
let on_login = on_login.clone();
|
||||||
let on_event_context_menu = on_event_context_menu.clone();
|
let on_event_context_menu = on_event_context_menu.clone();
|
||||||
let on_calendar_context_menu = on_calendar_context_menu.clone();
|
let on_calendar_context_menu = on_calendar_context_menu.clone();
|
||||||
|
let view = view.clone();
|
||||||
|
|
||||||
match route {
|
match route {
|
||||||
Route::Home => {
|
Route::Home => {
|
||||||
@@ -62,6 +66,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
user_info={user_info}
|
user_info={user_info}
|
||||||
on_event_context_menu={on_event_context_menu}
|
on_event_context_menu={on_event_context_menu}
|
||||||
on_calendar_context_menu={on_calendar_context_menu}
|
on_calendar_context_menu={on_calendar_context_menu}
|
||||||
|
view={view}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -80,6 +85,8 @@ pub struct CalendarViewProps {
|
|||||||
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub view: ViewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
@@ -238,6 +245,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
user_info={props.user_info.clone()}
|
user_info={props.user_info.clone()}
|
||||||
on_event_context_menu={props.on_event_context_menu.clone()}
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
|
view={props.view.clone()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -250,6 +258,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
user_info={props.user_info.clone()}
|
user_info={props.user_info.clone()}
|
||||||
on_event_context_menu={props.on_event_context_menu.clone()}
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
|
view={props.view.clone()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
use web_sys::HtmlSelectElement;
|
||||||
use crate::services::calendar_service::UserInfo;
|
use crate::services::calendar_service::UserInfo;
|
||||||
use crate::components::CalendarListItem;
|
use crate::components::CalendarListItem;
|
||||||
|
|
||||||
@@ -13,6 +14,18 @@ pub enum Route {
|
|||||||
Calendar,
|
Calendar,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum ViewMode {
|
||||||
|
Month,
|
||||||
|
Week,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ViewMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
ViewMode::Month
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct SidebarProps {
|
pub struct SidebarProps {
|
||||||
pub user_info: Option<UserInfo>,
|
pub user_info: Option<UserInfo>,
|
||||||
@@ -23,10 +36,27 @@ pub struct SidebarProps {
|
|||||||
pub on_color_picker_toggle: Callback<String>,
|
pub on_color_picker_toggle: Callback<String>,
|
||||||
pub available_colors: Vec<String>,
|
pub available_colors: Vec<String>,
|
||||||
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
|
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
|
||||||
|
pub current_view: ViewMode,
|
||||||
|
pub on_view_change: Callback<ViewMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Sidebar)]
|
#[function_component(Sidebar)]
|
||||||
pub fn sidebar(props: &SidebarProps) -> Html {
|
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::<HtmlSelectElement>();
|
||||||
|
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! {
|
html! {
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -82,6 +112,14 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
<button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button">
|
<button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button">
|
||||||
{"+ Create Calendar"}
|
{"+ Create Calendar"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="view-selector">
|
||||||
|
<select class="view-selector-dropdown" onchange={on_view_change}>
|
||||||
|
<option value="month" selected={matches!(props.current_view, ViewMode::Month)}>{"Month"}</option>
|
||||||
|
<option value="week" selected={matches!(props.current_view, ViewMode::Week)}>{"Week"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
137
src/components/week_view.rs
Normal file
137
src/components/week_view.rs
Normal file
@@ -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<NaiveDate, Vec<CalendarEvent>>,
|
||||||
|
pub on_event_click: Callback<CalendarEvent>,
|
||||||
|
#[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, CalendarEvent)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<NaiveDate> = (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! {
|
||||||
|
<div class="week-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>
|
||||||
|
|
||||||
|
// 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! {
|
||||||
|
<div
|
||||||
|
class={classes!("calendar-day", if is_today { Some("today") } else { None })}
|
||||||
|
oncontextmenu={
|
||||||
|
if let Some(callback) = &props.on_calendar_context_menu {
|
||||||
|
let callback = callback.clone();
|
||||||
|
let date = *date;
|
||||||
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
callback.emit((e, date));
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="day-number">{date.day()}</div>
|
||||||
|
<div class="day-events">
|
||||||
|
{
|
||||||
|
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! {
|
||||||
|
<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>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}).collect::<Html>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
59
styles.css
59
styles.css
@@ -462,6 +462,19 @@ body {
|
|||||||
background: white;
|
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 {
|
.weekday-header {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -773,6 +786,15 @@ body {
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-selector {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-selector-dropdown {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -889,6 +911,43 @@ body {
|
|||||||
transform: translateY(0);
|
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 */
|
/* Create Calendar Modal */
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user