first commit
This commit is contained in:
157
src/app.rs
Normal file
157
src/app.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct Option {
|
||||
value: &'static str,
|
||||
name: &'static str,
|
||||
topic: &'static str,
|
||||
}
|
||||
|
||||
pub const OPTIONS: &[Option] = &[
|
||||
Option {
|
||||
value: "citerne",
|
||||
name: "Citerne",
|
||||
topic: "home/citerne/height",
|
||||
},
|
||||
Option {
|
||||
value: "eau",
|
||||
name: "Eau",
|
||||
topic: "home/eau/volume",
|
||||
},
|
||||
Option {
|
||||
value: "elec-hight",
|
||||
name: "Electricité heure pleine",
|
||||
topic: "home/elec-hight/pleine",
|
||||
},
|
||||
Option {
|
||||
value: "elec-low",
|
||||
name: "Electricité heure creuse",
|
||||
topic: "home/elec-low/creuse",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Value {
|
||||
id: u16,
|
||||
service: String,
|
||||
capteur: String,
|
||||
type_donnee: String,
|
||||
donnee: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
struct HeftyData {
|
||||
capteur: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
use leptos::server_fn::ServerFnError;
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
|
||||
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Router>
|
||||
<nav class="px-2 py-1 flex gap-3">
|
||||
<a href="/">Home</a>
|
||||
<a href="/formulaire">Formulaire</a>
|
||||
</nav>
|
||||
<main class="dark:bg-slate-900 text-gray-50 h-screen">
|
||||
<Routes>
|
||||
<Route path="" view= move || view! { <Home/> }/>
|
||||
<Route path="/formulaire" view= move || view! { <Form/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! {
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Form() -> impl IntoView {
|
||||
let add_value = create_server_action::<AddValue>();
|
||||
let value = add_value.value();
|
||||
let has_error = move || value.with(|val| matches!(val, Some(Err(_))));
|
||||
|
||||
view! {
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Form"</h2>
|
||||
|
||||
<ActionForm action=add_value>
|
||||
<div>
|
||||
<label class="block">Capteur</label>
|
||||
<select name="hefty_arg[capteur]">
|
||||
{OPTIONS.into_iter()
|
||||
.map(|option| view! { <option value={option.value}>{option.name}</option>})
|
||||
.collect::<Vec<_>>()}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block">Valeur</label>
|
||||
<input type="text" name="hefty_arg[value]" />
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Valider</button>
|
||||
</div>
|
||||
</ActionForm>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[server(AddValue, "/api/add-value")]
|
||||
pub async fn add_value(hefty_arg: HeftyData) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
let mut conn = db().await?;
|
||||
|
||||
dbg!(&hefty_arg);
|
||||
|
||||
match sqlx::query("INSERT INTO donnees (service, donnee) VALUES ($1, $2)")
|
||||
.bind(hefty_arg.capteur)
|
||||
.bind(hefty_arg.value)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod todo;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::todo::*;
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
|
||||
leptos::mount::hydrate_body(TodoApp);
|
||||
}
|
||||
69
src/main.rs
Normal file
69
src/main.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
mod todo;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
pub use crate::todo::*;
|
||||
pub use actix_files::Files;
|
||||
pub use actix_web::*;
|
||||
pub use leptos::prelude::*;
|
||||
pub use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/style.css")]
|
||||
pub async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use self::{ssr::*, todo::ssr::*};
|
||||
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
println!("listening on http://{}", &addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(TodoApp);
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.leptos_routes(routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || {
|
||||
use leptos::prelude::*;
|
||||
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<AutoReload options=leptos_options.clone()/>
|
||||
<HydrationScripts options=leptos_options.clone()/>
|
||||
</head>
|
||||
<body>
|
||||
<TodoApp/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
})
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
172
src/todo.rs
Normal file
172
src/todo.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use leptos::either::Either;
|
||||
use leptos::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
//use server_fn::ServerFnError;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
use leptos::server_fn::ServerFnError;
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req_parts = use_context::<leptos_actix::Request>();
|
||||
|
||||
if let Some(req_parts) = req_parts {
|
||||
println!("Path = {:?}", req_parts.path());
|
||||
}
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
// Lines below show how to set status code and headers on the response
|
||||
// let resp = expect_context::<ResponseOptions>();
|
||||
// resp.set_status(StatusCode::IM_A_TEAPOT);
|
||||
// resp.insert_header(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
let mut conn = db().await?;
|
||||
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Todos/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos() -> impl IntoView {
|
||||
let add_todo = ServerMultiAction::<AddTodo>::new();
|
||||
let submissions = add_todo.submissions();
|
||||
let delete_todo = ServerAction::<DeleteTodo>::new();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = Resource::new(
|
||||
move || {
|
||||
(
|
||||
delete_todo.version().get(),
|
||||
add_todo.version().get(),
|
||||
delete_todo.version().get(),
|
||||
)
|
||||
},
|
||||
move |_| get_todos(),
|
||||
);
|
||||
|
||||
let existing_todos = move || {
|
||||
Suspend::new(async move {
|
||||
todos.await.map(|todos| {
|
||||
if todos.is_empty() {
|
||||
Either::Left(view! { <p>"No tasks were found."</p> })
|
||||
} else {
|
||||
Either::Right(
|
||||
todos
|
||||
.iter()
|
||||
.map(move |todo| {
|
||||
let id = todo.id;
|
||||
view! {
|
||||
<li>
|
||||
{todo.title.clone()} <ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value=id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>"Add a Todo" <input type="text" name="title"/></label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> }>
|
||||
// TODO: ErrorBoundary here seems to break Suspense in Actix
|
||||
// <ErrorBoundary fallback=|errors| view! { <p>"Error: " {move || format!("{:?}", errors.get())}</p> }>
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
<li class="pending">
|
||||
{move || submission.input().get().map(|data| data.title)}
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}}
|
||||
|
||||
</ul>
|
||||
// </ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user