Files
calendar/frontend/src/components/sidebar.rs
Connor Johnstone 85d23b0347
Some checks failed
Build and Push Docker Image / docker (push) Failing after 26s
Rebrand application from 'Calendar App' to 'Runway'
- Update project name in Cargo.toml from calendar-app to runway
- Change HTML title and sidebar header to 'Runway'
- Complete README rewrite with new branding and philosophy
- Add 'The Name' section explaining runway metaphor as passive infrastructure
- Update Dockerfile build references to use new binary name
- Maintain all technical documentation with new branding context

The name 'Runway' embodies passive infrastructure that enables coordination
without getting in the way - like airport runways that provide essential
structure for planes but stay invisible during flight.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 11:05:21 -04:00

246 lines
8.9 KiB
Rust

use crate::components::CalendarListItem;
use crate::services::calendar_service::UserInfo;
use web_sys::HtmlSelectElement;
use yew::prelude::*;
use yew_router::prelude::*;
#[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,
}
#[derive(Clone, PartialEq)]
pub enum Style {
Default,
Google,
}
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 Style {
pub fn value(&self) -> &'static str {
match self {
Style::Default => "default",
Style::Google => "google",
}
}
pub fn from_value(value: &str) -> Self {
match value {
"google" => Style::Google,
_ => Style::Default,
}
}
pub fn stylesheet_path(&self) -> Option<&'static str> {
match self {
Style::Default => None, // No additional stylesheet needed - uses base styles.css
Style::Google => Some("google.css"), // Trunk copies to root level
}
}
}
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>,
pub current_style: Style,
pub on_style_change: Callback<Style>,
}
#[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);
}
})
};
let on_style_change = {
let on_style_change = props.on_style_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_style = Style::from_value(&value);
on_style_change.emit(new_style);
}
})
};
html! {
<aside class="app-sidebar">
<div class="sidebar-header">
<h1>{"Runway"}</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>
<div class="style-selector">
<select class="style-selector-dropdown" onchange={on_style_change}>
<option value="default" selected={matches!(props.current_style, Style::Default)}>{"Default"}</option>
<option value="google" selected={matches!(props.current_style, Style::Google)}>{"Google Calendar"}</option>
</select>
</div>
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
</div>
</aside>
}
}