From 056cb951a2bfed5a21cc193bb819d251b09370d8 Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 2 Nov 2025 07:23:05 +0000 Subject: [PATCH] new plan: calendar day adder --- plan-proto/src/plan.rs | 5 + plan-server/src/main.rs | 5 +- plan/img/plan-icon.svg | 46 ++ plan/index.html | 2 + plan/index.scss | 198 ++++++++- plan/src/components/half_hour_range_select.rs | 35 +- plan/src/components/month.rs | 181 ++++++++ plan/src/components/nav.rs | 6 +- plan/src/components/new_plan_day.rs | 189 +++++++++ plan/src/components/planview.rs | 2 +- plan/src/main.rs | 13 + plan/src/pages/new_plan.rs | 393 +++++++++--------- 12 files changed, 854 insertions(+), 221 deletions(-) create mode 100644 plan/img/plan-icon.svg create mode 100644 plan/src/components/month.rs create mode 100644 plan/src/components/new_plan_day.rs diff --git a/plan-proto/src/plan.rs b/plan-proto/src/plan.rs index 77c6f78..0d98bbf 100644 --- a/plan-proto/src/plan.rs +++ b/plan-proto/src/plan.rs @@ -13,6 +13,8 @@ pub enum PlanError { StartAfterEnd, #[error("day 0 ends before the plan starts")] Day0EndsBeforePlanStarts, + #[error("plan has no days")] + NoDays, } crate::id_impl!(PlanId); @@ -25,6 +27,9 @@ pub struct CreatePlan { } impl CreatePlan { pub fn check(&self) -> Result<(), PlanError> { + if self.days.is_empty() { + return Err(PlanError::NoDays); + } let start_half_hour: HalfHour = self.start_time.time().into(); if let Some(day0) = self.days.get(&0u8) diff --git a/plan-server/src/main.rs b/plan-server/src/main.rs index 7d60fc6..6b13fd8 100644 --- a/plan-server/src/main.rs +++ b/plan-server/src/main.rs @@ -195,7 +195,10 @@ async fn main() { ), ) .with_state(state) - .layer(tower_http::cors::CorsLayer::permissive().allow_headers([header::AUTHORIZATION])) + .layer( + tower_http::cors::CorsLayer::permissive() + .allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]), + ) .fallback(get(handle_http_static)); let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap(); log::info!("listening on {}", listener.local_addr().unwrap()); diff --git a/plan/img/plan-icon.svg b/plan/img/plan-icon.svg new file mode 100644 index 0000000..595e550 --- /dev/null +++ b/plan/img/plan-icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/plan/index.html b/plan/index.html index aefe204..7f5ff31 100644 --- a/plan/index.html +++ b/plan/index.html @@ -6,6 +6,8 @@ plan + + diff --git a/plan/index.scss b/plan/index.scss index 44f5323..22c3ca3 100644 --- a/plan/index.scss +++ b/plan/index.scss @@ -222,17 +222,15 @@ nav.user-nav { align-items: center; gap: 10px; + .submit { + margin-top: 20px; -} + button { + padding: 5px 15px 5px 15px; + font-size: 2em; + } -.field { - display: flex; - flex-direction: column; - flex-wrap: nowrap; - // width: max-content; - // min-width: 60%; - font-size: 1.5em; - width: 100%; + } } .days { @@ -255,7 +253,15 @@ nav.user-nav { flex-direction: column; align-items: flex-start; flex-wrap: nowrap; - // gap: 10px; + + .field { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + font-size: 1.5em; + width: 100%; + } + .remove { width: 100%; @@ -386,11 +392,40 @@ nav.user-nav { } } +.plan-details { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + width: 100%; + + .add-day { + margin-top: 5px; + font-size: 1.5em; + padding: 5px 0 5px 0; + } +} + .fields { display: flex; - align-items: center; - flex-direction: column; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; gap: 10px; + width: 100%; + + .field { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + font-size: 1.5em; + flex-grow: 1; + + input, + select { + font-size: 1em; + box-sizing: border-box; + } + } } @media only screen and (max-width : 999px) { @@ -475,3 +510,142 @@ nav.user-nav { } } } + +.create-days-view { + display: flex; + flex-wrap: nowrap; + flex-direction: column; + width: 80vw; + gap: 20px; + + .controls { + align-self: flex-end; + + button { + font-size: 1.5em; + } + } + + .create-days-view-content {} +} + +.calendar-view { + width: 100%; + user-select: none; + display: flex; + flex-direction: column; + gap: 20px; + + .headline { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + width: 100%; + justify-content: space-between; + font-size: 2em; + align-items: baseline; + + button { + font-size: 1em; + padding: 0; + height: 2em; + width: 2em; + text-align: center; + } + + .inactive { + cursor: not-allowed; + background-color: rgba(255, 255, 255, 0.2); + color: white; + filter: brightness(40%); + + &:hover { + filter: brightness(25%); + } + } + } + + .days { + display: grid; + grid-template-columns: repeat(7, 1fr); + + .weekday { + text-align: center; + } + + .sun { + color: red; + } + + .calendar-day { + border: 1px solid white; + padding: 5px; + cursor: pointer; + font-size: 2em; + text-align: center; + + &.inactive { + cursor: not-allowed; + border: 1px solid rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); + color: white; + filter: brightness(40%); + + &:hover { + filter: brightness(40%); + } + } + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + } + + &.marked { + background-color: rgba(0, 255, 0, 0.3); + border: 1px solid rgba(0, 255, 0, 0.6); + // color: rgba(0, 255, 0, 0.6); + + &:hover { + background-color: rgba(0, 255, 0, 0.2); + } + } + } + } +} + +@media only screen and (max-width: 299px) { + + .days, + .headline { + font-size: 0.5rem !important; + } + + .days { + gap: 3px; + } +} + +@media only screen and (min-width: 300px) and (max-width : 425px) { + + .calendar-day, + .headline { + font-size: 1em !important; + } + + .days { + gap: 3px; + } +} + +input, +select { + border: 1px solid rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.07); + color: white; + + &:focus { + outline: 1px solid white; + background-color: white; + color: black; + } +} diff --git a/plan/src/components/half_hour_range_select.rs b/plan/src/components/half_hour_range_select.rs index b5cc44c..ed42958 100644 --- a/plan/src/components/half_hour_range_select.rs +++ b/plan/src/components/half_hour_range_select.rs @@ -35,7 +35,7 @@ pub fn HalfHourRangeSelect( .collect::(); let cb = on_change.clone(); let on_change_range = range.clone(); - let on_change = Callback::from(move |ev: Event| { + let on_change_cb = Callback::from(move |ev: Event| { if let Some(select) = ev.target_dyn_into::() { let selected = select.selected_index(); if selected == -1 { @@ -46,8 +46,39 @@ pub fn HalfHourRangeSelect( } } }); + let on_wheel = { + let on_change = on_change.clone(); + let range = range.clone(); + Callback::from(move |ev: WheelEvent| { + let Some(target) = ev.target_dyn_into::() else { + return; + }; + let index = target.selected_index(); + let new_index = match ev.delta_y().total_cmp(&0.0) { + core::cmp::Ordering::Equal => return, + core::cmp::Ordering::Less => { + if index != 0 { + index - 1 + } else { + (target.children().length() - 1) as i32 + } + } + core::cmp::Ordering::Greater => { + if index + 1 < target.children().length() as i32 { + index + 1 + } else { + 0 + } + } + }; + target.set_selected_index(new_index); + if let Some(selected) = range.clone().nth(new_index as _) { + on_change.emit(selected); + } + }) + }; html! { - {options} } diff --git a/plan/src/components/month.rs b/plan/src/components/month.rs new file mode 100644 index 0000000..8f2ac8c --- /dev/null +++ b/plan/src/components/month.rs @@ -0,0 +1,181 @@ +use core::str::FromStr; + +use chrono::{Datelike, Local, Month, Months, NaiveDate, Weekday}; +use yew::prelude::*; + +#[derive(Debug)] +pub enum MonthDayUpdate { + Remove, + Add, +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct MonthViewProps { + pub starting_date: UseStateHandle, + pub date_state: UseStateHandle, + pub marked_days: Box<[NaiveDate]>, + pub on_day_update: Callback<(u8, MonthDayUpdate)>, +} + +#[function_component] +pub fn MonthView( + MonthViewProps { + starting_date, + date_state, + marked_days, + on_day_update, + }: &MonthViewProps, +) -> Html { + let pointer_state = use_state(|| false); + + let on_pointerup = { + let pointer_state = pointer_state.clone(); + Callback::from(move |ev: PointerEvent| { + ev.set_cancel_bubble(true); + pointer_state.set(false); + }) + }; + let starting_date = + NaiveDate::from_str(starting_date.as_str()).unwrap_or_else(|_| Local::now().date_naive()); + let month = Month::try_from(date_state.month() as u8).expect("month from date"); + let year = date_state.year(); + let headlines = (0..7) + .map(|w| Weekday::try_from(w).expect("invalid weekday")) + .map(|w| { + let sun = (w == Weekday::Sun).then_some("sun"); + html! { + + {w.to_string()} + + } + }) + .collect::(); + let padding = { + let start_of_month = + NaiveDate::from_ymd_opt(year, date_state.month(), 1).unwrap_or_else(|| { + Local::now() + .date_naive() + .with_day(1) + .expect("cannot get first day") + }); + (0..start_of_month.weekday() as u8) + .map(|_| html! {
}) + .collect::() + }; + + let days = (1..=month.num_days(year).expect("get month days")) + .map(|day| { + let curr_day = NaiveDate::from_ymd_opt(year, date_state.month(), day as _); + let sun = curr_day + .as_ref() + .and_then(|d| (d.weekday() == Weekday::Sun).then_some("sun")); + let marked = curr_day + .as_ref() + .and_then(|date| marked_days.contains(date).then_some("marked")); + + let offset = curr_day + .map(|curr_day| curr_day.num_days_from_ce() - starting_date.num_days_from_ce()) + .and_then(|offset| (offset >= 0).then_some(offset as u8)); + + let inactive = offset.is_none().then_some("inactive"); + let on_pointerdown = offset.map(|offset| { + let marked = marked.is_some(); + let pointer_state = pointer_state.clone(); + let update = on_day_update.clone(); + Callback::from(move |ev: PointerEvent| { + ev.set_cancel_bubble(true); + let msg = ( + offset, + match marked { + true => MonthDayUpdate::Remove, + false => MonthDayUpdate::Add, + }, + ); + update.emit(msg); + pointer_state.set(true); + }) + }); + let on_pointerup = { + let pointer_state = pointer_state.clone(); + Callback::from(move |ev: PointerEvent| { + ev.set_cancel_bubble(true); + pointer_state.set(false); + }) + }; + let on_pointer_enter = offset.map(|offset| { + let marked = marked.is_some(); + let update = on_day_update.clone(); + let pointer_state = pointer_state.clone(); + + Callback::from(move |_| { + if !*pointer_state { + return; + } + update.emit(( + offset, + match marked { + true => MonthDayUpdate::Remove, + false => MonthDayUpdate::Add, + }, + )); + }) + }); + html! { +
+ {day} +
+ } + }) + .collect::(); + + let prev_month = { + let date_state = date_state.clone(); + date_state + .checked_sub_months(Months::new(1)) + .filter(|prev| { + (prev.month() as i32 + prev.year() * 100) + >= (starting_date.month() as i32 + starting_date.year() * 100) + }) + .map(|prev| { + Callback::from(move |_| { + date_state.set(prev); + }) + }) + }; + let prev_month_inactive = prev_month.is_none().then_some("inactive"); + let next_month = { + let date_state = date_state.clone(); + Callback::from(move |_| { + if let Some(next) = date_state.checked_add_months(Months::new(1)) { + date_state.set(next); + } + }) + }; + html! { +
+
+ + {month.name()} + {year.to_string()} + +
+
+ {headlines} + {padding} + {days} +
+
+ } +} diff --git a/plan/src/components/nav.rs b/plan/src/components/nav.rs index 05f767d..3dc6f53 100644 --- a/plan/src/components/nav.rs +++ b/plan/src/components/nav.rs @@ -69,7 +69,11 @@ pub fn Nav() -> Html { }); html! { diff --git a/plan/src/components/new_plan_day.rs b/plan/src/components/new_plan_day.rs new file mode 100644 index 0000000..75c4799 --- /dev/null +++ b/plan/src/components/new_plan_day.rs @@ -0,0 +1,189 @@ +use core::str::FromStr; +use std::collections::HashMap; + +use chrono::{Datelike, Days, Local, Month, NaiveDate}; +use plan_proto::{HalfHour, plan::CreatePlanDay}; +use yew::prelude::*; + +use crate::{ + components::{ + half_hour_range_select::HalfHourRangeSelect, + month::{MonthDayUpdate, MonthView}, + }, + weekday::FullWeekday, +}; + +pub enum DayUpdateAction { + New, + Remove, + Replace(CreatePlanDay), + IncrementDay, + DecrementDay, +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct NewPlanPlainProps { + pub days: HashMap, + pub date: UseStateHandle, + pub on_day_update: Callback<(u8, DayUpdateAction)>, +} +#[function_component] +pub fn NewPlanPlain( + NewPlanPlainProps { + days, + date, + on_day_update, + }: &NewPlanPlainProps, +) -> Html { + let mut days = days + .iter() + .map(|(offset, day)| { + let date = NaiveDate::from_str(date.as_str()) + .ok() + .and_then(|d| d.checked_add_days(Days::new(*offset as _))); + let detail = date.map(|date| { + html! { + {"("}{date.full_weekday()}{")"} + } + }); + let date_str = date + .map(|d| d.format("%-d %B %Y").to_string()) + .unwrap_or_else(|| format!("+{offset} day")); + let range = HalfHour::Hour0Min0..=HalfHour::Hour23Min30; + let on_start_change = { + let on_day_update = on_day_update.clone(); + let day = day.clone(); + let offset = *offset; + Callback::from(move |new_start| { + let mut day = day.clone(); + day.day_start = new_start; + on_day_update.emit((offset, DayUpdateAction::Replace(day))); + }) + }; + let on_end_change = { + let on_day_update = on_day_update.clone(); + let day = day.clone(); + let offset = *offset; + Callback::from(move |new_end| { + let mut day = day.clone(); + day.day_end = new_end; + on_day_update.emit((offset, DayUpdateAction::Replace(day))); + }) + }; + let minus_cb = (*offset > 0 && !days.contains_key(&(*offset - 1))).then(|| { + let on_day_update = on_day_update.clone(); + let offset = *offset; + Callback::from(move |_| { + on_day_update.emit((offset, DayUpdateAction::DecrementDay)); + }) + }); + let plus_cb = (*offset < u8::MAX && !days.contains_key(&(*offset + 1))).then(|| { + let on_day_update = on_day_update.clone(); + let offset = *offset; + Callback::from(move |_| { + on_day_update.emit((offset, DayUpdateAction::IncrementDay)); + }) + }); + + let yeet_cb = { + let offset = *offset; + let on_day_update = on_day_update.clone(); + Callback::from(move |_| { + on_day_update.emit((offset, DayUpdateAction::Remove)); + }) + }; + + ( + *offset, + html! { +
+
+ + {date_str} + +
+
+ {detail} +
+
+ + +
+
+ + +
+ +
+ }, + ) + }) + .collect::>(); + days.sort_by_key(|(offset, _)| *offset); + let days = days.into_iter().map(|(_, d)| d).collect::(); + html! { +
+ {days} +
+ } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct NewPlanCalendarDateAdderProps { + pub days: HashMap, + pub date: UseStateHandle, + pub on_day_update: Callback<(u8, DayUpdateAction)>, + pub calendar_page: UseStateHandle, +} + +#[function_component] +pub fn NewPlanCalendarDateAdder( + NewPlanCalendarDateAdderProps { + days, + date, + on_day_update, + calendar_page, + }: &NewPlanCalendarDateAdderProps, +) -> Html { + let start_date = NaiveDate::from_str(date.as_str()).expect("parsing date"); + let marked = days + .keys() + .copied() + .filter_map(|offset| start_date.checked_add_days(Days::new(offset as _))) + .collect::>(); + let on_day_update = { + let on_day_update = on_day_update.clone(); + Callback::from(move |(offset, update)| match update { + MonthDayUpdate::Remove => on_day_update.emit((offset, DayUpdateAction::Remove)), + MonthDayUpdate::Add => on_day_update.emit((offset, DayUpdateAction::New)), + }) + }; + html! { +
+ +
+ } +} diff --git a/plan/src/components/planview.rs b/plan/src/components/planview.rs index e01c45f..8305eb5 100644 --- a/plan/src/components/planview.rs +++ b/plan/src/components/planview.rs @@ -202,7 +202,7 @@ pub fn Day( .collect::(); html! { -
+
{date.to_string()} {date.full_weekday()}
    Html { + if SERVER_URL.starts_with("http://") { + let doc = gloo::utils::document(); + let title = doc.title(); + doc.set_title( + format!( + "{} — LOCAL", + title.strip_suffix(" — LOCAL").unwrap_or(title.as_str()), + ) + .as_str(), + ); + } match route { Route::Signup => { return html! { diff --git a/plan/src/pages/new_plan.rs b/plan/src/pages/new_plan.rs index 1c619b6..d86d433 100644 --- a/plan/src/pages/new_plan.rs +++ b/plan/src/pages/new_plan.rs @@ -1,10 +1,7 @@ -use core::{ - ops::{Not, RangeBounds}, - str::FromStr, -}; +use core::{ops::Not, str::FromStr}; use std::collections::HashMap; -use chrono::{Datelike, Days, Local, NaiveDate, NaiveTime, SubsecRound, Timelike, Utc}; +use chrono::{Local, NaiveDate, NaiveTime, SubsecRound, Timelike, Utc}; use gloo::net::http::Request; use plan_proto::{ HalfHour, @@ -20,14 +17,19 @@ use crate::{ components::{ error::ErrorDisplay, half_hour_range_select::HalfHourRangeSelect, + new_plan_day::{DayUpdateAction, NewPlanCalendarDateAdder, NewPlanPlain}, state_input::{InputType, StateInput}, }, pages::mainpage::SignedOutMainPage, request::RequestError, storage::StorageKey, - weekday::FullWeekday, }; +enum DaysView { + SingleDay, + Month, +} + #[function_component] pub fn NewPlan() -> Html { let token = match use_context::() { @@ -74,154 +76,38 @@ pub fn NewPlan() -> Html { title: None, days: HashMap::new(), }); - let add_day = { - let add_days_plan = plan.clone(); - let end_time = end_time.clone(); - Callback::from(move |_| { - let mut plan = (*add_days_plan).clone(); - let mut offset = plan - .days - .keys() - .last() - .map(|offset| *offset + 1) - .unwrap_or_default(); - while plan.days.contains_key(&offset) { - offset += 1; - } - plan.days.insert( - offset, - CreatePlanDay { - day_start: plan.start_time.time().into(), - day_end: *end_time, - }, - ); - add_days_plan.set(plan); - }) - }; - let mut days = plan - .days - .iter() - .map(|(offset, day)| { - let date = NaiveDate::from_str(date.as_str()) - .ok() - .and_then(|d| d.checked_add_days(Days::new(*offset as _))); - let detail = date.map(|date| { - html! { - {"("}{date.full_weekday()}{")"} - } - }); - let date_str = date - .map(|d| d.to_string()) - .unwrap_or_else(|| format!("+{offset} day")); - let range = { - let start: HalfHour = plan.start_time.time().into(); - start..=*end_time - }; - let on_start_plan = plan.clone(); - let on_start_offset = *offset; - let on_start_change = Callback::from(move |new_start| { - let mut plan_update = (*on_start_plan).clone(); - if let Some(day) = plan_update.days.get_mut(&on_start_offset) { - day.day_start = new_start; - } - on_start_plan.set(plan_update); - }); - let on_end_plan = plan.clone(); - let on_end_offset = *offset; - let on_end_change = Callback::from(move |new_end| { - let mut plan_update = (*on_end_plan).clone(); - if let Some(day) = plan_update.days.get_mut(&on_end_offset) { - day.day_end = new_end; - } - on_end_plan.set(plan_update); - }); - let minus_cb = (*offset > 0 && !plan.days.contains_key(&(*offset - 1))).then(|| { - let offset = *offset; - let plan = plan.clone(); - Callback::from(move |_| { - let mut p = (*plan).clone(); - if let Some(day) = p.days.remove(&offset) { - p.days.insert(offset - 1, day); - } - plan.set(p); - }) - }); - let plus_cb = - (*offset < u8::MAX && !plan.days.contains_key(&(*offset + 1))).then(|| { - let offset = *offset; - let plan = plan.clone(); - Callback::from(move |_| { - let mut p = (*plan).clone(); - if let Some(day) = p.days.remove(&offset) { - p.days.insert(offset + 1, day); - } - plan.set(p); - }) - }); + // let add_day = { + // let add_days_plan = plan.clone(); + // let end_time = end_time.clone(); + // Callback::from(move |_| { + // let mut plan = (*add_days_plan).clone(); + // let mut offset = plan + // .days + // .keys() + // .last() + // .map(|offset| *offset + 1) + // .unwrap_or_default(); + // while plan.days.contains_key(&offset) { + // offset += 1; + // } + // plan.days.insert( + // offset, + // CreatePlanDay { + // day_start: plan.start_time.time().into(), + // day_end: *end_time, + // }, + // ); + // add_days_plan.set(plan); + // }) + // }; - let yeet_cb = { - let offset = *offset; - let plan = plan.clone(); - Callback::from(move |_| { - let mut p = (*plan).clone(); - p.days.remove(&offset); - plan.set(p); - }) - }; - - ( - *offset, - html! { -
    -
    - - {date_str} - -
    -
    - {detail} -
    -
    - - -
    -
    - - -
    - -
    - }, - ) - }) - .collect::>(); - days.sort_by_key(|(offset, _)| *offset); - let days = days.into_iter().map(|(_, d)| d).collect::(); let start_time_range = HalfHour::Hour0Min0..=*end_time; let end_time_range = { let limited_start = NaiveDate::from_str(date.as_str()) .map(|d| d <= Local::now().date_naive()) .unwrap_or_default(); if limited_start { - let start: HalfHour = Local::now().time().into(); + let start: HalfHour = plan.start_time.time().into(); start..=HalfHour::Hour23Min30 } else { HalfHour::Hour0Min0..=HalfHour::Hour23Min30 @@ -246,9 +132,7 @@ pub fn NewPlan() -> Html { } p.days.values_mut().for_each(|day| { - if day.day_start < new_start { - day.day_start = new_start; - } + day.day_start = new_start; }); plan.set(p); @@ -261,9 +145,7 @@ pub fn NewPlan() -> Html { end.set(new); let mut p = (*plan).clone(); p.days.values_mut().for_each(|d| { - if d.day_end > new { - d.day_end = new; - } + d.day_end = new; }); plan.set(p); }) @@ -276,18 +158,21 @@ pub fn NewPlan() -> Html { let title = title.clone(); Callback::from(move |_| { let mut plan = (*plan).clone(); - let title = match ClampedString::new(title.trim().to_string()) { - Ok(title) => title, - Err(range) => { - let total_len = title.trim().chars().count(); - on_err.set(Some(format!( - "title length must be between {} characters; not {total_len}", - range.bounds_string_human() - ))); - return; - } + plan.title = if title.trim().is_empty() { + None + } else { + Some(match ClampedString::new(title.trim().to_string()) { + Ok(title) => title, + Err(range) => { + let total_len = title.trim().chars().count(); + on_err.set(Some(format!( + "title length must be between {} characters; not {total_len}", + range.bounds_string_human() + ))); + return; + } + }) }; - plan.title = title.is_empty().not().then_some(title); let req = match Request::post(format!("{SERVER_URL}/s/plans").as_str()) .header("authorization", format!("Bearer {}", token.token).as_str()) .header("content-type", CBOR_CONTENT_TYPE) @@ -320,53 +205,153 @@ pub fn NewPlan() -> Html { } } }); - todo!() }) }; + let on_day_update = { + let plan = plan.clone(); + let end_time = end_time.clone(); + Callback::from(move |(offset, update)| { + let mut p = (*plan).clone(); + match update { + DayUpdateAction::New => { + p.days.insert( + offset, + CreatePlanDay { + day_start: plan.start_time.time().into(), + day_end: *end_time, + }, + ); + } + DayUpdateAction::Remove => { + p.days.remove(&offset); + } + DayUpdateAction::Replace(day) => { + p.days.insert(offset, day); + } + DayUpdateAction::IncrementDay => { + if offset < u8::MAX + && !p.days.contains_key(&(offset + 1)) + && let Some(day) = p.days.remove(&offset) + { + p.days.insert(offset + 1, day); + } + } + DayUpdateAction::DecrementDay => { + if offset > 0 + && !p.days.contains_key(&(offset - 1)) + && let Some(day) = p.days.remove(&offset) + { + p.days.insert(offset - 1, day); + } + } + } + plan.set(p); + }) + }; + + let view = use_state(|| DaysView::Month); + let calendar_page_date = + use_state(|| NaiveDate::from_str(date.as_str()).expect("parsing date")); + let days_view = { + let content = match &*view { + DaysView::SingleDay => html! { + + }, + DaysView::Month => html! { + + }, + }; + let controls = match &*view { + DaysView::SingleDay => { + let on_click = { + let view = view.clone(); + Callback::from(move |_| { + view.set(DaysView::Month); + }) + }; + html! { + + } + } + DaysView::Month => { + let on_click = { + let view = view.clone(); + Callback::from(move |_| { + view.set(DaysView::SingleDay); + }) + }; + let disabled = plan.days.is_empty(); + html! { + + } + } + }; + + html! { +
    +
    + {controls} +
    +
    + {content} +
    +
    + } + }; + html! {

    {"new plan"}

    -
    -
    - - +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -
    - {days} + //
    + {days_view}