Add web frontend in Yew
This commit is contained in:
@@ -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
1
web/css/uikit.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
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
1
web/js/uikit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
web/src/components/mod.rs
Normal file
2
web/src/components/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod task;
|
||||
pub mod tasks_list;
|
||||
190
web/src/components/task.rs
Normal file
190
web/src/components/task.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
38
web/src/components/tasks_list.rs
Normal file
38
web/src/components/tasks_list.rs
Normal 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
62
web/src/lib.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user