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
		Add a link
		
	
		Reference in a new issue