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

82
Cargo.lock generated
View File

@@ -486,6 +486,11 @@ dependencies = [
name = "contextswitch-web"
version = "0.1.0"
dependencies = [
"contextswitch",
"reqwasm",
"serde",
"uikit-rs",
"wasm-bindgen-futures",
"yew",
]
@@ -524,9 +529,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
"libc",
]
@@ -803,6 +808,26 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.1.0"
@@ -1048,9 +1073,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.119"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
[[package]]
name = "linked-hash-map"
@@ -1145,9 +1170,9 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba42135c6a5917b9db9cd7b293e5409e1c6b041e6f9825e92e55a894c63b6f8"
checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
dependencies = [
"libc",
"log",
@@ -1195,13 +1220,12 @@ dependencies = [
[[package]]
name = "nom"
version = "7.1.0"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
"version_check",
]
[[package]]
@@ -1244,9 +1268,9 @@ dependencies = [
[[package]]
name = "num_threads"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c539a50b93a303167eded6e8dff5220cd39447409fb659f4cd24b1f72fe4f133"
checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
dependencies = [
"libc",
]
@@ -1501,9 +1525,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.15"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57"
dependencies = [
"proc-macro2",
]
@@ -1592,10 +1616,19 @@ dependencies = [
]
[[package]]
name = "reqwest"
version = "0.11.9"
name = "reqwasm"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb"
dependencies = [
"gloo-net",
]
[[package]]
name = "reqwest"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
dependencies = [
"base64",
"bytes",
@@ -1847,9 +1880,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.86"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54"
dependencies = [
"proc-macro2",
"quote",
@@ -2160,6 +2193,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "uikit-rs"
version = "0.1.0"
source = "git+https://github.com/dax/uikit-rs.git#2414ccb2878a1cbec7bbe32b8ebd217b0684d0a8"
dependencies = [
"regex",
"yew",
]
[[package]]
name = "unicode-bidi"
version = "0.3.7"
@@ -2397,9 +2439,9 @@ checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
[[package]]
name = "winreg"
version = "0.7.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]

View File

@@ -21,7 +21,7 @@ condition = {}
workspace = false
[tasks.run-api]
install_crate = "bunyan"
install_crate = { crate_name = "bunyan", binary = "bunyan" }
env = { "CONFIG_PATH" = "api/config", "TASKRC" = "$PWD/api/taskrc" }
command = "bash"
args = ["-c", "cargo run -p contextswitch-api | bunyan"]
@@ -29,7 +29,7 @@ workspace = false
watch = { watch = ["./api/"], no_git_ignore = true }
[tasks.run-web]
install_crate = "trunk"
install_crate = { crate_name = "trunk", binary = "trunk" }
command = "bash"
args = ["-c", "cd web; trunk serve"]
workspace = false

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