new plan: calendar day adder

This commit is contained in:
emilis 2025-11-02 07:23:05 +00:00
parent 0bac7ae0b4
commit 056cb951a2
No known key found for this signature in database
12 changed files with 854 additions and 221 deletions

View File

@ -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)

View File

@ -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());

46
plan/img/plan-icon.svg Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="59.999996mm"
height="60.000004mm"
viewBox="0 0 59.999996 60.000004"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-1.8520831,-183.06459)">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.860999"
id="rect1-2-7"
width="60"
height="60"
x="1.8520833"
y="183.06459" />
<rect
style="fill:#003000;fill-opacity:1;stroke:none;stroke-width:0.860999"
id="rect3-2-5"
width="60"
height="50"
x="1.8520833"
y="188.06459" />
<rect
style="fill:#004800;fill-opacity:1;stroke:none;stroke-width:0.860999"
id="rect4-7-9"
width="60"
height="30"
x="1.8520833"
y="198.06459" />
<rect
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-dasharray:none;stroke-opacity:1"
id="rect6-0-7-2"
width="58.5"
height="15"
x="2.6020832"
y="205.56459" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>plan</title>
<link data-trunk rel="sass" href="index.scss" />
<link rel="icon" href="/img/plan-icon.svg" />
<link data-trunk rel="copy-dir" href="img">
</head>
<body>

View File

@ -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;
}
}

View File

@ -35,7 +35,7 @@ pub fn HalfHourRangeSelect(
.collect::<Html>();
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::<HtmlSelectElement>() {
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::<HtmlSelectElement>() 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! {
<select onchange={on_change} id={id.clone()} required=true>
<select onwheel={on_wheel} onchange={on_change_cb} id={id.clone()} required=true>
{options}
</select>
}

View File

@ -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<String>,
pub date_state: UseStateHandle<NaiveDate>,
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! {
<span class={classes!("weekday", sun)}>
{w.to_string()}
</span>
}
})
.collect::<Html>();
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! {<div />})
.collect::<Html>()
};
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! {
<div
class={classes!("calendar-day", marked, sun, inactive)}
onpointerdown={on_pointerdown.clone()}
onpointerup={on_pointerup.clone()}
onpointerenter={on_pointer_enter}
>
<span class="number">{day}</span>
</div>
}
})
.collect::<Html>();
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! {
<div class="calendar-view">
<div class="headline">
<button class={classes!(prev_month_inactive)} onclick={prev_month}>
{""}
</button>
<span class="month">{month.name()}</span>
<span class="year">{year.to_string()}</span>
<button onclick={next_month}>{""}</button>
</div>
<div
class="days"
onpointerup={on_pointerup.clone()}
onpointerleave={on_pointerup.clone()}
onpointercancel={on_pointerup.clone()}
>
{headlines}
{padding}
{days}
</div>
</div>
}
}

View File

@ -69,7 +69,11 @@ pub fn Nav() -> Html {
});
html! {
<nav class="user-nav">
<a href="/"><button>{"home"}</button></a>
<a href="/">
<button>
{"home"}
</button>
</a>
{user}
{logged_in_buttons}
</nav>

View File

@ -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<u8, CreatePlanDay>,
pub date: UseStateHandle<String>,
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! {
<span>{"("}{date.full_weekday()}{")"}</span>
}
});
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! {
<div class="create-day">
<div class="set-date">
<button
disabled={minus_cb.is_none()}
onclick={minus_cb.unwrap_or_default()}
>
{"-"}
</button>
<span class="date">{date_str}</span>
<button
disabled={plus_cb.is_none()}
onclick={plus_cb.unwrap_or_default()}
>{"+"}</button>
</div>
<div class="date-detail">
{detail}
</div>
<div class="field">
<label for={format!("start-time-{offset}")}>{"start time"}</label>
<HalfHourRangeSelect
id={format!("start-time-{offset}")}
range={range.clone()}
selected={day.day_start}
on_change={on_start_change}
/>
</div>
<div class="field">
<label for={format!("end-time-{offset}")}>{"end time"}</label>
<HalfHourRangeSelect
id={format!("end-time-{offset}")}
range={range.clone()}
selected={day.day_end}
on_change={on_end_change}
/>
</div>
<button class="remove" onclick={yeet_cb}>{"remove"}</button>
</div>
},
)
})
.collect::<Box<[_]>>();
days.sort_by_key(|(offset, _)| *offset);
let days = days.into_iter().map(|(_, d)| d).collect::<Html>();
html! {
<div class="days">
{days}
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct NewPlanCalendarDateAdderProps {
pub days: HashMap<u8, CreatePlanDay>,
pub date: UseStateHandle<String>,
pub on_day_update: Callback<(u8, DayUpdateAction)>,
pub calendar_page: UseStateHandle<NaiveDate>,
}
#[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::<Box<[_]>>();
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! {
<div class="calendar-adder">
<MonthView
starting_date={date.clone()}
date_state={calendar_page.clone()}
marked_days={marked}
on_day_update={on_day_update}
/>
</div>
}
}

View File

@ -202,7 +202,7 @@ pub fn Day(
.collect::<Html>();
html! {
<div class="day">
<div class="day" onpointerup={on_pointerup.clone()} onpointerleave={on_pointerup.clone()}>
<span class="date">{date.to_string()}</span>
<span class="weekday">{date.full_weekday()}</span>
<ol

View File

@ -10,7 +10,9 @@ mod components {
pub mod dialog;
pub mod error;
pub mod half_hour_range_select;
pub mod month;
pub mod nav;
pub mod new_plan_day;
pub mod planview;
pub mod state_input;
}
@ -71,6 +73,17 @@ enum Route {
}
fn route(route: Route) -> 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! {

View File

@ -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::<Token>() {
@ -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! {
<span>{"("}{date.full_weekday()}{")"}</span>
}
});
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! {
<div class="create-day">
<div class="set-date">
<button
disabled={minus_cb.is_none()}
onclick={minus_cb.unwrap_or_default()}
>
{"-"}
</button>
<span class="date">{date_str}</span>
<button
disabled={plus_cb.is_none()}
onclick={plus_cb.unwrap_or_default()}
>{"+"}</button>
</div>
<div class="date-detail">
{detail}
</div>
<div class="field">
<label for={format!("start-time-{offset}")}>{"start time"}</label>
<HalfHourRangeSelect
id={format!("start-time-{offset}")}
range={range.clone()}
selected={day.day_start}
on_change={on_start_change}
/>
</div>
<div class="field">
<label for={format!("end-time-{offset}")}>{"end time"}</label>
<HalfHourRangeSelect
id={format!("end-time-{offset}")}
range={range.clone()}
selected={day.day_end}
on_change={on_end_change}
/>
</div>
<button class="remove" onclick={yeet_cb}>{"remove"}</button>
</div>
},
)
})
.collect::<Box<[_]>>();
days.sort_by_key(|(offset, _)| *offset);
let days = days.into_iter().map(|(_, d)| d).collect::<Html>();
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;
}
});
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;
}
});
plan.set(p);
})
@ -276,7 +158,10 @@ 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()) {
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();
@ -286,8 +171,8 @@ pub fn NewPlan() -> Html {
)));
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,12 +205,113 @@ 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! {
<NewPlanPlain
days={plan.days.clone()}
date={date.clone()}
on_day_update={on_day_update}
/>
},
DaysView::Month => html! {
<NewPlanCalendarDateAdder
days={plan.days.clone()}
date={date.clone()}
on_day_update={on_day_update}
calendar_page={calendar_page_date}
/>
},
};
let controls = match &*view {
DaysView::SingleDay => {
let on_click = {
let view = view.clone();
Callback::from(move |_| {
view.set(DaysView::Month);
})
};
html! {
<button onclick={on_click}>{"select days"}</button>
}
}
DaysView::Month => {
let on_click = {
let view = view.clone();
Callback::from(move |_| {
view.set(DaysView::SingleDay);
})
};
let disabled = plan.days.is_empty();
html! {
<button disabled={disabled} onclick={on_click}>{"modify selected days"}</button>
}
}
};
html! {
<div class="create-days-view">
<div class="controls">
{controls}
</div>
<div class="create-days-view-content">
{content}
</div>
</div>
}
};
html! {
<div class="new-plan">
<h1>{"new plan"}</h1>
<div class="plan-details">
<div class="fields">
<div class="field">
<label for="title">
@ -336,7 +322,7 @@ pub fn NewPlan() -> Html {
</div>
<div class="field">
<label for="date">{"start date"}</label>
<StateInput id={Some("date".to_string())} state={date} input_type={InputType::Date}/>
<StateInput id={Some("date".to_string())} state={date.clone()} input_type={InputType::Date}/>
</div>
<div class="field">
<label for="time">
@ -362,11 +348,10 @@ pub fn NewPlan() -> Html {
selected={*end_time}
/>
</div>
<button onclick={add_day}>{"add day"}</button>
</div>
<div class="days">
{days}
// <button class="add-day" onclick={add_day}>{"add day"}</button>
</div>
{days_view}
<div class="submit">
<button onclick={on_submit}>{"submit"}</button>
</div>