Add web frontend in Yew

This commit is contained in:
2022-03-21 17:10:51 +01:00
parent 5958a3ed29
commit c746325495
12 changed files with 374 additions and 30 deletions

View File

@@ -4,9 +4,17 @@ version = "0.1.0"
edition = "2021"
authors = ["David Rousselie <david@rousselie.name>"]
[lib]
path = "src/lib.rs"
[[bin]]
path = "src/main.rs"
name = "contextswitch-web"
[dependencies]
contextswitch = { path = ".." }
yew = "0.19"
reqwasm = "0.5"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"
uikit-rs = { git = "https://github.com/dax/uikit-rs.git" }

1
web/css/uikit.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,5 +3,11 @@
<head>
<meta charset="utf-8" />
<title>Contextswitch</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/uikit.min.css" />
<script src="js/uikit.min.js"></script>
<script src="js/uikit-icons.min.js"></script>
<link data-trunk rel="copy-dir" href="css" />
<link data-trunk rel="copy-dir" href="js" />
</head>
</html>

1
web/js/uikit-icons.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
web/js/uikit.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
pub mod task;
pub mod tasks_list;

190
web/src/components/task.rs Normal file
View File

@@ -0,0 +1,190 @@
use contextswitch;
use uikit_rs as uk;
use yew::{classes, function_component, html, Callback, Classes, Html, MouseEvent, Properties};
#[derive(Properties, PartialEq)]
pub struct TaskProps {
pub task: contextswitch::Task,
#[prop_or_default]
pub selected: bool,
#[prop_or_default]
pub on_task_select: Callback<Option<contextswitch::Task>>,
}
#[function_component(Task)]
pub fn task(
TaskProps {
task,
selected,
on_task_select,
}: &TaskProps,
) -> Html {
let toggle_details = {
let task = task.clone();
let on_task_select = on_task_select.clone();
let is_task_selected = *selected;
Callback::from(move |_| {
on_task_select.emit(if is_task_selected {
None
} else {
Some(task.clone())
})
})
};
let text_style = if task.status == contextswitch::Status::Completed {
uk::Text::Success
} else {
uk::Text::Emphasis
};
let arrow = if *selected {
uk::IconType::TriangleDown
} else {
uk::IconType::TriangleRight
};
let task_status_class: Classes = format!("task-status-{}", task.status).into();
let bookmark_count = if let Some(contextswitch) = &task.contextswitch {
contextswitch.bookmarks.len()
} else {
0
};
html! {
<uk::Card size={uk::CardSize::Small}
style={uk::CardStyle::Default}
hover={true}
width={uk::Width::_1_1}
class={task_status_class}>
<uk::CardBody padding={vec![uk::Padding::RemoveVertical]}
margin={vec![uk::Margin::SmallTop, uk::Margin::SmallBottom]}>
<uk::Grid gap_size={uk::GridGapSize::Small}
vertical_alignement={uk::FlexVerticalAlignement::Middle}>
<uk::Icon icon_type={uk::IconType::Check}
text_style={vec![text_style]} />
<TaskDescription task={task.clone()}
onclick={toggle_details.clone()} />
<uk::IconNav>
<li>
<uk::Icon icon_type={uk::IconType::FileEdit} href="#" />
</li>
<li>
<uk::Link href="#" onclick={toggle_details.clone()}>
<uk::Icon icon_type={uk::IconType::Bookmark} />
<span> {bookmark_count}</span>
</uk::Link>
</li>
</uk::IconNav>
<uk::Icon icon_type={arrow} href="#" onclick={toggle_details} />
</uk::Grid>
{
if *selected {
html! {
<TaskDetails task={task.clone()} />
}
} else { html! {} }
}
</uk::CardBody>
</uk::Card>
}
}
#[derive(Properties, PartialEq)]
pub struct TaskDescriptionProps {
pub task: contextswitch::Task,
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
}
#[function_component(TaskDescription)]
pub fn task_description(TaskDescriptionProps { task, onclick }: &TaskDescriptionProps) -> Html {
html! {
<uk::Flex width={uk::Width::_Expand} onclick={onclick}>
<uk::CardTitle text_style={vec![uk::Text::Lighter]}>
{task.description.clone()}
</uk::CardTitle>
</uk::Flex>
}
}
#[derive(Properties, PartialEq)]
pub struct TaskDetailsProps {
pub task: contextswitch::Task,
}
#[function_component(TaskDetails)]
pub fn task_details(TaskDetailsProps { task }: &TaskDetailsProps) -> Html {
let priority = task
.priority
.as_ref()
.map(|prio| prio.to_string())
.unwrap_or_else(|| "-".to_string());
let project = task
.project
.as_ref()
.map(|proj| proj.to_string())
.unwrap_or_else(|| "-".to_string());
html! {
<div class={classes!(uk::Margin::Small)}>
<uk::Divider margin={vec![uk::Margin::Small]} />
{
if let Some(contextswitch) = &task.contextswitch {
html! {
<TaskContextswitch contextswitch={contextswitch.clone()} />
}
} else { html! {} }
}
<uk::Grid gap_size={uk::GridGapSize::Small}
child_width={uk::ChildWidth::_Expand}>
<span class={classes!(uk::Text::Meta)}>
{ format!("priority: {}", priority) }
</span>
<span class={classes!(uk::Text::Meta)}>
{ format!("project: {}", project) }
</span>
</uk::Grid>
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct TaskContextswitchProps {
pub contextswitch: contextswitch::ContextswitchData,
}
#[function_component(TaskContextswitch)]
pub fn task_contextswitch(
TaskContextswitchProps { contextswitch }: &TaskContextswitchProps,
) -> Html {
html! {
<uk::Grid gap_size={uk::GridGapSize::Small} height_match={true}>
{
contextswitch.bookmarks.iter().map(|bookmark| {
html! {
<TaskBookmark bookmark={bookmark.clone()} />
}
}).collect::<Html>()
}
<uk::Icon icon_type={uk::IconType::Plus}
margin={vec![uk::Margin::Remove]} />
</uk::Grid>
}
}
#[derive(Properties, PartialEq)]
pub struct TaskBookmarkProps {
pub bookmark: contextswitch::Bookmark,
}
#[function_component(TaskBookmark)]
pub fn task_bookmark(TaskBookmarkProps { bookmark }: &TaskBookmarkProps) -> Html {
html! {
<div class={classes!(uk::Width::_1_1, uk::Text::Small, uk::Margin::Remove)}>
<uk::Grid gap_size={uk::GridGapSize::Small} vertical_alignement={uk::FlexVerticalAlignement::Middle}>
<uk::Icon icon_type={uk::IconType::Bookmark} />
<uk::Link href={bookmark.uri.to_string()}>{bookmark.uri.to_string()}</uk::Link>
</uk::Grid>
</div>
}
}

View File

@@ -0,0 +1,38 @@
use crate::components::task;
use contextswitch::Task;
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct TasksListProps {
#[prop_or_default]
pub tasks: Vec<Task>,
#[prop_or_default]
pub selected_task: Option<Task>,
#[prop_or_default]
pub on_task_select: Callback<Option<Task>>,
}
#[function_component(TasksList)]
pub fn tasks_list(
TasksListProps {
tasks,
selected_task,
on_task_select,
}: &TasksListProps,
) -> Html {
tasks
.iter()
.map(|task| {
let task_is_selected = selected_task
.clone()
.map(|t| t.id == task.id)
.unwrap_or(false);
html! {
<task::Task selected={task_is_selected}
on_task_select={on_task_select}
task={task.clone()} />
}
})
.collect()
}

62
web/src/lib.rs Normal file
View File

@@ -0,0 +1,62 @@
use components::tasks_list::TasksList;
use contextswitch::Task;
use reqwasm::http::Request;
use uikit_rs as uk;
use yew::prelude::*;
mod components;
#[function_component(App)]
pub fn app() -> Html {
let tasks = use_state(Vec::new);
{
let tasks = tasks.clone();
use_effect_with_deps(
move |_| {
wasm_bindgen_futures::spawn_local(async move {
let fetched_tasks: Vec<Task> =
Request::get("http://localhost:8000/tasks?filter=task")
.send()
.await
.unwrap() // TODO
.json()
.await
.unwrap(); // TODO
tasks.set(fetched_tasks);
});
|| ()
},
(),
);
}
let selected_task = use_state(|| None);
let on_task_select = {
let selected_task = selected_task.clone();
Callback::from(move |task: Option<Task>| {
selected_task.set(task);
})
};
html! {
<uk::Section style={uk::SectionStyle::Default}>
<uk::Container size={uk::ContainerSize::Small}>
<uk::Filter target=".status-filter"
filter_width={uk::Width::_Expand}
filter_component={uk::UIKitComponent::SubNav}
filter_class={"uk-subnav-pill"}
filters={vec![uk::FilterData { class: "".to_string(), label: "all".to_string() },
uk::FilterData { class: ".task-status-pending".to_string(), label: "pending".to_string() },
uk::FilterData { class: ".task-status-completed".to_string(), label: "completed".to_string() }]}>
<uk::Grid gap_size={uk::GridGapSize::Small}
margin={vec![uk::Margin::Default]}
height_match={true}
class={"status-filter"}>
<TasksList tasks={(*tasks).clone()}
selected_task={(*selected_task).clone()}
on_task_select={on_task_select} />
</uk::Grid>
</uk::Filter>
</uk::Container>
</uk::Section>
}
}

View File

@@ -1,11 +1,4 @@
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello World" }</h1>
}
}
use contextswitch_web::App;
fn main() {
yew::start_app::<App>();