- Created calendar-models/ shared library with RFC 5545-compliant VEvent structures - Migrated backend to use shared VEvent with proper field mappings (dtstart/dtend, rrule, exdate, etc.) - Converted CalDAV client to parse into VEvent structures with structured types - Updated all CRUD handlers to use VEvent with CalendarUser, Attendee, VAlarm types - Restructured project as Cargo workspace with frontend/, backend/, calendar-models/ - Updated Trunk configuration for new directory structure - Fixed all compilation errors and field references throughout codebase - Updated documentation and build instructions for workspace structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
7.4 KiB
Rust
195 lines
7.4 KiB
Rust
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
use web_sys::HtmlSelectElement;
|
|
use crate::services::calendar_service::UserInfo;
|
|
use crate::components::CalendarListItem;
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
pub enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/login")]
|
|
Login,
|
|
#[at("/calendar")]
|
|
Calendar,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum ViewMode {
|
|
Month,
|
|
Week,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum Theme {
|
|
Default,
|
|
Ocean,
|
|
Forest,
|
|
Sunset,
|
|
Purple,
|
|
Dark,
|
|
Rose,
|
|
Mint,
|
|
}
|
|
|
|
impl Theme {
|
|
|
|
pub fn value(&self) -> &'static str {
|
|
match self {
|
|
Theme::Default => "default",
|
|
Theme::Ocean => "ocean",
|
|
Theme::Forest => "forest",
|
|
Theme::Sunset => "sunset",
|
|
Theme::Purple => "purple",
|
|
Theme::Dark => "dark",
|
|
Theme::Rose => "rose",
|
|
Theme::Mint => "mint",
|
|
}
|
|
}
|
|
|
|
pub fn from_value(value: &str) -> Self {
|
|
match value {
|
|
"ocean" => Theme::Ocean,
|
|
"forest" => Theme::Forest,
|
|
"sunset" => Theme::Sunset,
|
|
"purple" => Theme::Purple,
|
|
"dark" => Theme::Dark,
|
|
"rose" => Theme::Rose,
|
|
"mint" => Theme::Mint,
|
|
_ => Theme::Default,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ViewMode {
|
|
fn default() -> Self {
|
|
ViewMode::Month
|
|
}
|
|
}
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct SidebarProps {
|
|
pub user_info: Option<UserInfo>,
|
|
pub on_logout: Callback<()>,
|
|
pub on_create_calendar: Callback<()>,
|
|
pub color_picker_open: Option<String>,
|
|
pub on_color_change: Callback<(String, String)>,
|
|
pub on_color_picker_toggle: Callback<String>,
|
|
pub available_colors: Vec<String>,
|
|
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
|
|
pub current_view: ViewMode,
|
|
pub on_view_change: Callback<ViewMode>,
|
|
pub current_theme: Theme,
|
|
pub on_theme_change: Callback<Theme>,
|
|
}
|
|
|
|
#[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::<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);
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_theme_change = {
|
|
let on_theme_change = props.on_theme_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_theme = Theme::from_value(&value);
|
|
on_theme_change.emit(new_theme);
|
|
}
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<aside class="app-sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>{"Calendar App"}</h1>
|
|
{
|
|
if let Some(ref info) = props.user_info {
|
|
html! {
|
|
<div class="user-info">
|
|
<div class="username">{&info.username}</div>
|
|
<div class="server-url">{&info.server_url}</div>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! { <div class="user-info loading">{"Loading..."}</div> }
|
|
}
|
|
}
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<Link<Route> to={Route::Calendar} classes="nav-link">{"Calendar"}</Link<Route>>
|
|
</nav>
|
|
{
|
|
if let Some(ref info) = props.user_info {
|
|
if !info.calendars.is_empty() {
|
|
html! {
|
|
<div class="calendar-list">
|
|
<h3>{"My Calendars"}</h3>
|
|
<ul>
|
|
{
|
|
info.calendars.iter().map(|cal| {
|
|
html! {
|
|
<CalendarListItem
|
|
calendar={cal.clone()}
|
|
color_picker_open={props.color_picker_open.as_ref() == Some(&cal.path)}
|
|
on_color_change={props.on_color_change.clone()}
|
|
on_color_picker_toggle={props.on_color_picker_toggle.clone()}
|
|
available_colors={props.available_colors.clone()}
|
|
on_context_menu={props.on_calendar_context_menu.clone()}
|
|
/>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! { <div class="no-calendars">{"No calendars found"}</div> }
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
<div class="sidebar-footer">
|
|
<button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button">
|
|
{"+ Create Calendar"}
|
|
</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>
|
|
|
|
<div class="theme-selector">
|
|
<select class="theme-selector-dropdown" onchange={on_theme_change}>
|
|
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"🎨 Default"}</option>
|
|
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"🌊 Ocean"}</option>
|
|
<option value="forest" selected={matches!(props.current_theme, Theme::Forest)}>{"🌲 Forest"}</option>
|
|
<option value="sunset" selected={matches!(props.current_theme, Theme::Sunset)}>{"🌅 Sunset"}</option>
|
|
<option value="purple" selected={matches!(props.current_theme, Theme::Purple)}>{"💜 Purple"}</option>
|
|
<option value="dark" selected={matches!(props.current_theme, Theme::Dark)}>{"🌙 Dark"}</option>
|
|
<option value="rose" selected={matches!(props.current_theme, Theme::Rose)}>{"🌹 Rose"}</option>
|
|
<option value="mint" selected={matches!(props.current_theme, Theme::Mint)}>{"🍃 Mint"}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
|
</div>
|
|
</aside>
|
|
}
|
|
} |