yew: initial todo app skeleton
This commit is contained in:
parent
4ed90a4403
commit
e439bddad0
7 changed files with 7874 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target/
|
||||||
|
dist/
|
1519
Cargo.lock
generated
Normal file
1519
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
28
Cargo.toml
Normal file
28
Cargo.toml
Normal 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"
|
10
graphql/all_users_todos.graphql
Normal file
10
graphql/all_users_todos.graphql
Normal 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
6087
graphql/schema.json
Normal file
File diff suppressed because it is too large
Load diff
12
index.html
Normal file
12
index.html
Normal 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
216
src/main.rs
Normal 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>();
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue