new plan: calendar day adder
This commit is contained in:
parent
0bac7ae0b4
commit
056cb951a2
|
|
@ -13,6 +13,8 @@ pub enum PlanError {
|
||||||
StartAfterEnd,
|
StartAfterEnd,
|
||||||
#[error("day 0 ends before the plan starts")]
|
#[error("day 0 ends before the plan starts")]
|
||||||
Day0EndsBeforePlanStarts,
|
Day0EndsBeforePlanStarts,
|
||||||
|
#[error("plan has no days")]
|
||||||
|
NoDays,
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::id_impl!(PlanId);
|
crate::id_impl!(PlanId);
|
||||||
|
|
@ -25,6 +27,9 @@ pub struct CreatePlan {
|
||||||
}
|
}
|
||||||
impl CreatePlan {
|
impl CreatePlan {
|
||||||
pub fn check(&self) -> Result<(), PlanError> {
|
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();
|
let start_half_hour: HalfHour = self.start_time.time().into();
|
||||||
|
|
||||||
if let Some(day0) = self.days.get(&0u8)
|
if let Some(day0) = self.days.get(&0u8)
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,10 @@ async fn main() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_state(state)
|
.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));
|
.fallback(get(handle_http_static));
|
||||||
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
||||||
log::info!("listening on {}", listener.local_addr().unwrap());
|
log::info!("listening on {}", listener.local_addr().unwrap());
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -6,6 +6,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>plan</title>
|
<title>plan</title>
|
||||||
<link data-trunk rel="sass" href="index.scss" />
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
198
plan/index.scss
198
plan/index.scss
|
|
@ -222,17 +222,15 @@ nav.user-nav {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
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 {
|
.days {
|
||||||
|
|
@ -255,7 +253,15 @@ nav.user-nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
// gap: 10px;
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
font-size: 1.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
width: 100%;
|
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 {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: row;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
gap: 10px;
|
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) {
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub fn HalfHourRangeSelect(
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
let cb = on_change.clone();
|
let cb = on_change.clone();
|
||||||
let on_change_range = range.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>() {
|
if let Some(select) = ev.target_dyn_into::<HtmlSelectElement>() {
|
||||||
let selected = select.selected_index();
|
let selected = select.selected_index();
|
||||||
if selected == -1 {
|
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! {
|
html! {
|
||||||
<select onchange={on_change} id={id.clone()} required=true>
|
<select onwheel={on_wheel} onchange={on_change_cb} id={id.clone()} required=true>
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,11 @@ pub fn Nav() -> Html {
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<nav class="user-nav">
|
<nav class="user-nav">
|
||||||
<a href="/"><button>{"home"}</button></a>
|
<a href="/">
|
||||||
|
<button>
|
||||||
|
{"home"}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
{user}
|
{user}
|
||||||
{logged_in_buttons}
|
{logged_in_buttons}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ pub fn Day(
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
|
|
||||||
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="date">{date.to_string()}</span>
|
||||||
<span class="weekday">{date.full_weekday()}</span>
|
<span class="weekday">{date.full_weekday()}</span>
|
||||||
<ol
|
<ol
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ mod components {
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod half_hour_range_select;
|
pub mod half_hour_range_select;
|
||||||
|
pub mod month;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
|
pub mod new_plan_day;
|
||||||
pub mod planview;
|
pub mod planview;
|
||||||
pub mod state_input;
|
pub mod state_input;
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +73,17 @@ enum Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route(route: Route) -> Html {
|
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 {
|
match route {
|
||||||
Route::Signup => {
|
Route::Signup => {
|
||||||
return html! {
|
return html! {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
use core::{
|
use core::{ops::Not, str::FromStr};
|
||||||
ops::{Not, RangeBounds},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
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 gloo::net::http::Request;
|
||||||
use plan_proto::{
|
use plan_proto::{
|
||||||
HalfHour,
|
HalfHour,
|
||||||
|
|
@ -20,14 +17,19 @@ use crate::{
|
||||||
components::{
|
components::{
|
||||||
error::ErrorDisplay,
|
error::ErrorDisplay,
|
||||||
half_hour_range_select::HalfHourRangeSelect,
|
half_hour_range_select::HalfHourRangeSelect,
|
||||||
|
new_plan_day::{DayUpdateAction, NewPlanCalendarDateAdder, NewPlanPlain},
|
||||||
state_input::{InputType, StateInput},
|
state_input::{InputType, StateInput},
|
||||||
},
|
},
|
||||||
pages::mainpage::SignedOutMainPage,
|
pages::mainpage::SignedOutMainPage,
|
||||||
request::RequestError,
|
request::RequestError,
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
weekday::FullWeekday,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum DaysView {
|
||||||
|
SingleDay,
|
||||||
|
Month,
|
||||||
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn NewPlan() -> Html {
|
pub fn NewPlan() -> Html {
|
||||||
let token = match use_context::<Token>() {
|
let token = match use_context::<Token>() {
|
||||||
|
|
@ -74,154 +76,38 @@ pub fn NewPlan() -> Html {
|
||||||
title: None,
|
title: None,
|
||||||
days: HashMap::new(),
|
days: HashMap::new(),
|
||||||
});
|
});
|
||||||
let add_day = {
|
// let add_day = {
|
||||||
let add_days_plan = plan.clone();
|
// let add_days_plan = plan.clone();
|
||||||
let end_time = end_time.clone();
|
// let end_time = end_time.clone();
|
||||||
Callback::from(move |_| {
|
// Callback::from(move |_| {
|
||||||
let mut plan = (*add_days_plan).clone();
|
// let mut plan = (*add_days_plan).clone();
|
||||||
let mut offset = plan
|
// let mut offset = plan
|
||||||
.days
|
// .days
|
||||||
.keys()
|
// .keys()
|
||||||
.last()
|
// .last()
|
||||||
.map(|offset| *offset + 1)
|
// .map(|offset| *offset + 1)
|
||||||
.unwrap_or_default();
|
// .unwrap_or_default();
|
||||||
while plan.days.contains_key(&offset) {
|
// while plan.days.contains_key(&offset) {
|
||||||
offset += 1;
|
// offset += 1;
|
||||||
}
|
// }
|
||||||
plan.days.insert(
|
// plan.days.insert(
|
||||||
offset,
|
// offset,
|
||||||
CreatePlanDay {
|
// CreatePlanDay {
|
||||||
day_start: plan.start_time.time().into(),
|
// day_start: plan.start_time.time().into(),
|
||||||
day_end: *end_time,
|
// day_end: *end_time,
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
add_days_plan.set(plan);
|
// 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 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 start_time_range = HalfHour::Hour0Min0..=*end_time;
|
||||||
let end_time_range = {
|
let end_time_range = {
|
||||||
let limited_start = NaiveDate::from_str(date.as_str())
|
let limited_start = NaiveDate::from_str(date.as_str())
|
||||||
.map(|d| d <= Local::now().date_naive())
|
.map(|d| d <= Local::now().date_naive())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if limited_start {
|
if limited_start {
|
||||||
let start: HalfHour = Local::now().time().into();
|
let start: HalfHour = plan.start_time.time().into();
|
||||||
start..=HalfHour::Hour23Min30
|
start..=HalfHour::Hour23Min30
|
||||||
} else {
|
} else {
|
||||||
HalfHour::Hour0Min0..=HalfHour::Hour23Min30
|
HalfHour::Hour0Min0..=HalfHour::Hour23Min30
|
||||||
|
|
@ -246,9 +132,7 @@ pub fn NewPlan() -> Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.days.values_mut().for_each(|day| {
|
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);
|
plan.set(p);
|
||||||
|
|
@ -261,9 +145,7 @@ pub fn NewPlan() -> Html {
|
||||||
end.set(new);
|
end.set(new);
|
||||||
let mut p = (*plan).clone();
|
let mut p = (*plan).clone();
|
||||||
p.days.values_mut().for_each(|d| {
|
p.days.values_mut().for_each(|d| {
|
||||||
if d.day_end > new {
|
d.day_end = new;
|
||||||
d.day_end = new;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
plan.set(p);
|
plan.set(p);
|
||||||
})
|
})
|
||||||
|
|
@ -276,18 +158,21 @@ pub fn NewPlan() -> Html {
|
||||||
let title = title.clone();
|
let title = title.clone();
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_| {
|
||||||
let mut plan = (*plan).clone();
|
let mut plan = (*plan).clone();
|
||||||
let title = match ClampedString::new(title.trim().to_string()) {
|
plan.title = if title.trim().is_empty() {
|
||||||
Ok(title) => title,
|
None
|
||||||
Err(range) => {
|
} else {
|
||||||
let total_len = title.trim().chars().count();
|
Some(match ClampedString::new(title.trim().to_string()) {
|
||||||
on_err.set(Some(format!(
|
Ok(title) => title,
|
||||||
"title length must be between {} characters; not {total_len}",
|
Err(range) => {
|
||||||
range.bounds_string_human()
|
let total_len = title.trim().chars().count();
|
||||||
)));
|
on_err.set(Some(format!(
|
||||||
return;
|
"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())
|
let req = match Request::post(format!("{SERVER_URL}/s/plans").as_str())
|
||||||
.header("authorization", format!("Bearer {}", token.token).as_str())
|
.header("authorization", format!("Bearer {}", token.token).as_str())
|
||||||
.header("content-type", CBOR_CONTENT_TYPE)
|
.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! {
|
||||||
|
<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! {
|
html! {
|
||||||
<div class="new-plan">
|
<div class="new-plan">
|
||||||
<h1>{"new plan"}</h1>
|
<h1>{"new plan"}</h1>
|
||||||
<div class="fields">
|
<div class="plan-details">
|
||||||
<div class="field">
|
<div class="fields">
|
||||||
<label for="title">
|
<div class="field">
|
||||||
{"title"}
|
<label for="title">
|
||||||
<span class="faint">{" (optional)"}</span>
|
{"title"}
|
||||||
</label>
|
<span class="faint">{" (optional)"}</span>
|
||||||
<StateInput id={Some("title".to_string())} state={title.clone()} />
|
</label>
|
||||||
|
<StateInput id={Some("title".to_string())} state={title.clone()} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="date">{"start date"}</label>
|
||||||
|
<StateInput id={Some("date".to_string())} state={date.clone()} input_type={InputType::Date}/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="time">
|
||||||
|
{"start time"}
|
||||||
|
<span class="faint">{" (all days)"}</span>
|
||||||
|
</label>
|
||||||
|
<HalfHourRangeSelect
|
||||||
|
id="time"
|
||||||
|
range={start_time_range}
|
||||||
|
on_change={on_plan_start_update}
|
||||||
|
selected={start_selected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="time-end">
|
||||||
|
{"end time"}
|
||||||
|
<span class="faint">{" (all days)"}</span>
|
||||||
|
</label>
|
||||||
|
<HalfHourRangeSelect
|
||||||
|
id="time-end"
|
||||||
|
range={end_time_range}
|
||||||
|
on_change={on_plan_end_change}
|
||||||
|
selected={*end_time}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
// <button class="add-day" onclick={add_day}>{"add day"}</button>
|
||||||
<label for="date">{"start date"}</label>
|
|
||||||
<StateInput id={Some("date".to_string())} state={date} input_type={InputType::Date}/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="time">
|
|
||||||
{"start time"}
|
|
||||||
<span class="faint">{" (all days)"}</span>
|
|
||||||
</label>
|
|
||||||
<HalfHourRangeSelect
|
|
||||||
id="time"
|
|
||||||
range={start_time_range}
|
|
||||||
on_change={on_plan_start_update}
|
|
||||||
selected={start_selected}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="time-end">
|
|
||||||
{"end time"}
|
|
||||||
<span class="faint">{" (all days)"}</span>
|
|
||||||
</label>
|
|
||||||
<HalfHourRangeSelect
|
|
||||||
id="time-end"
|
|
||||||
range={end_time_range}
|
|
||||||
on_change={on_plan_end_change}
|
|
||||||
selected={*end_time}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button onclick={add_day}>{"add day"}</button>
|
|
||||||
</div>
|
|
||||||
<div class="days">
|
|
||||||
{days}
|
|
||||||
</div>
|
</div>
|
||||||
|
{days_view}
|
||||||
<div class="submit">
|
<div class="submit">
|
||||||
<button onclick={on_submit}>{"submit"}</button>
|
<button onclick={on_submit}>{"submit"}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue