yew: initial todo app skeleton

This commit is contained in:
Vladan Popovic 2022-10-23 03:22:33 +02:00
parent 4ed90a4403
commit e439bddad0
7 changed files with 7874 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
dist/

1519
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

28
Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "todo-yew-hasura"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
graphql_client = { version = "0.11.0" }
serde_json = "1.0.87"
wasm-bindgen = "0.2.83"
wasm-bindgen-futures = "0.4.33"
web-sys = { version = "0.3.60", features = [
"Request",
"RequestInit",
"RequestMode",
"Response",
] }
yew = "0.19.3"
yew-router = "0.16.0"
gloo = "0.7"
gloo-utils = "0.1"
serde = { version = "1.0.147", features = ["derive"] }
console_error_panic_hook = "0.1.7"
reqwest = { version = "^0.11", features = ["json"] }
log = "0.4.3"
env_logger = "0.5.10"

View File

@ -0,0 +1,10 @@
query AllUsersTodos {
users {
id
name
todos(order_by: {created_at: asc}, limit: 5) {
title
created_at
}
}
}

6087
graphql/schema.json Normal file

File diff suppressed because it is too large Load Diff

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Todo app with hasura and yew frontend</title>
<style>
.menu a {
padding: 5px;
};
</style>
</head>
</html>

216
src/main.rs Normal file
View File

@ -0,0 +1,216 @@
use std::{
error::Error,
fmt::{self, Debug, Display, Formatter},
};
use graphql_client::GraphQLQuery;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
use serde_json::{Value, from_str, json, from_value};
use serde::Deserialize;
use yew::prelude::*;
use yew_router::prelude::*;
type Timestamptz = String;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.json",
query_path = "graphql/all_users_todos.graphql",
response_derives = "Debug",
normalization = "rust"
)]
struct AllUsersTodos;
#[derive(Debug, Clone, PartialEq)]
pub struct FetchError {
err: JsValue,
}
impl Display for FetchError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Debug::fmt(&self.err, f)
}
}
impl Error for FetchError {}
impl From<JsValue> for FetchError {
fn from(value: JsValue) -> Self {
Self { err: value }
}
}
pub enum FetchState<T> {
NotFetching,
Fetching,
Success(T),
Failed(FetchError),
}
pub enum Msg {
SetState(FetchState<all_users_todos::ResponseData>),
GetData,
}
pub async fn fetch_gql_data<T: for<'a> Deserialize<'a>>(query: &str) -> Result<T, FetchError> {
let mut req_opts = RequestInit::new();
req_opts.method("POST");
req_opts.body(Some(&JsValue::from_str(query)));
req_opts.mode(RequestMode::Cors);
let request = Request::new_with_str_and_init("http://localhost:8080/v1/graphql", &req_opts)?;
let window = gloo_utils::window();
let resp_value =
JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().unwrap();
let resp_text = JsFuture::from(resp.text()?).await?;
let data_str = resp_text.as_string().unwrap();
let data_value: Value = from_str(&data_str).unwrap();
from_value(data_value["data"].clone())
.map_err(|e| FetchError { err: JsValue::from(format!("{}", e)) })
}
#[derive(Routable, PartialEq, Clone, Debug)]
pub enum Routes {
#[at("/users")]
Users,
#[at("/todos")]
Todos,
#[at("/")]
Index,
}
struct App;
#[function_component(NavGlobal)]
fn nav_global() -> Html {
html! {
<div class={classes!("menu")}>
<Link<Routes> to={ Routes::Index }> { "Index" }</Link<Routes>>
<Link<Routes> to={ Routes::Users }> { "Users" }</Link<Routes>>
<Link<Routes> to={ Routes::Todos }> { "Todos" }</Link<Routes>>
</div>
}
}
struct AllUsersTodosComponent {
data: FetchState<all_users_todos::ResponseData>,
}
fn view_all_users_todos(all_users: &all_users_todos::ResponseData) -> Html {
html! {
<>
<h1>{ "All users and their todos" }</h1>
{
all_users.users.iter().map(|user| {
html! {
<>
<h3>{ user.name.to_owned() }</h3>
<ul>
{
user.todos.iter().map(|todo| {
html! {
<li>
<span>{ todo.title.to_owned() }</span>
<span>{ todo.created_at.to_owned() }</span>
</li>
}
}).collect::<Html>()
}
</ul>
</>
}
}).collect::<Html>()
}
</>
}
}
impl Component for AllUsersTodosComponent {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self { data: FetchState::NotFetching }
}
fn view(&self, _ctx: &Context<Self>) -> Html {
match &self.data {
FetchState::NotFetching => html! { "NotFetching" },
FetchState::Fetching => html! { "Fetching" },
FetchState::Success(all_users_todos_data) => view_all_users_todos(all_users_todos_data),
FetchState::Failed(err) => html! { err },
}
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
ctx.link().send_message(Msg::GetData);
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SetState(fetch_state) => {
self.data = fetch_state;
true
}
Msg::GetData => {
ctx.link().send_future(async {
match fetch_gql_data(&users_todos_query().await).await
{
Ok(data) => Msg::SetState(FetchState::Success(data)),
Err(err) => Msg::SetState(FetchState::Failed(err)),
}
});
ctx.link().send_message(Msg::SetState(FetchState::Fetching));
false
}
}
}
}
async fn users_todos_query() -> String {
let build_query = AllUsersTodos::build_query(all_users_todos::Variables {});
json!(build_query).to_string()
}
fn navigate_to_view(target: &Routes) -> Html {
match target {
Routes::Users => html!{ "TODO: All Users" },
// Routes::Todos => html!{ <Todos /> },
// Routes::Index => html!{ <Home /> },
Routes::Todos => html!{ <div>{ "TODO: All Todos" }</div> },
Routes::Index => html!{ <AllUsersTodosComponent /> },
}
}
impl Component for App {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<BrowserRouter>
<NavGlobal />
<main class="ps-relative t64">
<Switch<Routes> render={ Switch::render(navigate_to_view) } />
</main>
</BrowserRouter>
}
}
}
fn main() {
yew::start_app::<App>();
}