codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)
overhaul dioxus fullstack using a new implementation of server_fn This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bluetooth-scanner"
|
||||
version = "0.1.1"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,90 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut scan = use_action(|| async {
|
||||
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter};
|
||||
|
||||
let manager = btleplug::platform::Manager::new().await?;
|
||||
|
||||
// get the first bluetooth adapter
|
||||
let adapters = manager.adapters().await?;
|
||||
let central = adapters
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("No Bluetooth adapter found")?;
|
||||
|
||||
// start scanning for devices
|
||||
central.start_scan(ScanFilter::default()).await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Return the list of peripherals after scanning
|
||||
let mut devices = vec![];
|
||||
for p in central.peripherals().await? {
|
||||
if let Some(p) = p.properties().await? {
|
||||
devices.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort them by RSSI (signal strength)
|
||||
devices.sort_by_key(|p| p.rssi.unwrap_or(-100));
|
||||
|
||||
dioxus::Ok(devices)
|
||||
});
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/assets/tailwind.css") }
|
||||
div {
|
||||
div { class: "py-8 px-6",
|
||||
div { class: "container px-4 mx-auto",
|
||||
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
|
||||
button {
|
||||
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
|
||||
disabled: scan.is_pending(),
|
||||
onclick: move |_| {
|
||||
scan.call();
|
||||
},
|
||||
if scan.is_pending() { "Scanning" } else { "Scan" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section { class: "py-8",
|
||||
div { class: "container px-4 mx-auto",
|
||||
div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto",
|
||||
table { class: "table-auto w-full",
|
||||
thead {
|
||||
tr { class: "text-xs text-gray-500 text-left",
|
||||
th { class: "pl-6 pb-3 font-medium", "Strength" }
|
||||
th { class: "pb-3 font-medium", "Network" }
|
||||
th { class: "pb-3 font-medium", "Channel" }
|
||||
th { class: "pb-3 px-2 font-medium", "Security" }
|
||||
}
|
||||
}
|
||||
match scan.result() {
|
||||
None if scan.is_pending() => rsx! { "Scanning..." },
|
||||
None => rsx! { "Press Scan to start scanning" },
|
||||
Some(Err(_err)) => rsx! { "Failed to scan" },
|
||||
Some(Ok(peripherals)) => rsx! {
|
||||
tbody {
|
||||
for peripheral in peripherals.read().iter().rev() {
|
||||
tr { class: "text-xs bg-gray-50",
|
||||
td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" }
|
||||
td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" }
|
||||
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } }
|
||||
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -54,7 +54,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
div { id: "wrapper",
|
||||
div { class: "app",
|
||||
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
use dioxus::desktop::tao::dpi::LogicalSize;
|
||||
use dioxus::desktop::{Config, WindowBuilder};
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::html::MouseEvent;
|
||||
use dioxus::html::input_data::keyboard_types::Key;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
@@ -29,10 +29,7 @@ fn app() -> Element {
|
||||
let mut state = use_signal(Calculator::new);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/calculator.css"),
|
||||
}
|
||||
Stylesheet { href: asset!("/examples/assets/calculator.css") }
|
||||
div { id: "wrapper",
|
||||
div { class: "app",
|
||||
div {
|
||||
@@ -16,7 +16,7 @@ fn app() -> Element {
|
||||
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
|
||||
div { id: "controls",
|
||||
button { onclick: move |_| counters.write().push(0), "Add counter" }
|
||||
@@ -20,20 +20,12 @@ fn main() {
|
||||
}))
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
Stylesheet {
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
||||
crossorigin: "anonymous",
|
||||
}
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/crm.css"),
|
||||
}
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/examples/assets/crm.css"),
|
||||
}
|
||||
Stylesheet { href: asset!("/examples/assets/crm.css") }
|
||||
h1 { "Dioxus CRM Example" }
|
||||
Router::<Route> {}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog.
|
||||
//!
|
||||
//! This app combines `use_loader` and `use_action` to fetch data from the Dog API.
|
||||
//! - `use_loader` automatically fetches the list of dog breeds when the component mounts.
|
||||
//! - `use_action` fetches a random dog image whenever the `.dispatch` method is called.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
|
||||
let breed_list = use_loader(move || async move {
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
||||
struct ListBreeds {
|
||||
message: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
reqwest::get("https://dog.ceo/api/breeds/list/all")
|
||||
.await?
|
||||
.json::<ListBreeds>()
|
||||
.await
|
||||
})?;
|
||||
|
||||
// Whenever this action is called, it will re-run the future and return the result.
|
||||
let mut breed = use_action(move |breed| async move {
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
struct DogApi {
|
||||
message: String,
|
||||
}
|
||||
|
||||
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<DogApi>()
|
||||
.await
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "Doggo selector" }
|
||||
div { width: "400px",
|
||||
for cur_breed in breed_list.read().message.keys().take(20).cloned() {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
breed.call(cur_breed.clone());
|
||||
},
|
||||
"{cur_breed}"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
match breed.result() {
|
||||
None => rsx! { div { "Click the button to fetch a dog!" } },
|
||||
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
||||
Some(Ok(res)) => rsx! {
|
||||
img {
|
||||
max_width: "500px",
|
||||
max_height: "500px",
|
||||
src: "{res.read().message}"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "ecommerce-site"
|
||||
version = "0.1.1"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+25
-75
@@ -1,9 +1,27 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use dioxus::prelude::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_product(product_id: usize) -> Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> Result<Vec<Product>> {
|
||||
Ok(reqwest::get(format!(
|
||||
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
|
||||
pub(crate) struct Product {
|
||||
@@ -54,48 +72,8 @@ impl Display for Sort {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_user_carts(user_id: usize) -> Result<Vec<Cart>, reqwest::Error> {
|
||||
reqwest::get(format!(
|
||||
"https://fakestoreapi.com/carts/user/{user_id}?startdate=2019-12-10&enddate=2023-01-01"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_user(user_id: usize) -> dioxus::Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/users/{user_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_product(product_id: usize) -> dioxus::Result<Product> {
|
||||
Ok(
|
||||
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache up to 100 requests, invalidating them after 60 seconds
|
||||
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> dioxus::Result<Vec<Product>> {
|
||||
Ok(reqwest::get(format!(
|
||||
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
|
||||
))
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct User {
|
||||
struct User {
|
||||
id: usize,
|
||||
email: String,
|
||||
username: String,
|
||||
@@ -104,14 +82,6 @@ pub(crate) struct User {
|
||||
phone: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn fetch_most_recent_cart(&self) -> Result<Option<Cart>, reqwest::Error> {
|
||||
let all_carts = fetch_user_carts(self.id).await?;
|
||||
|
||||
Ok(all_carts.into_iter().max_by_key(|cart| cart.date))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FullName {
|
||||
firstname: String,
|
||||
@@ -119,7 +89,7 @@ struct FullName {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct Cart {
|
||||
struct Cart {
|
||||
id: usize,
|
||||
#[serde(rename = "userId")]
|
||||
user_id: usize,
|
||||
@@ -128,29 +98,9 @@ pub(crate) struct Cart {
|
||||
date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Cart {
|
||||
async fn update_database(&mut self) -> Result<(), reqwest::Error> {
|
||||
let id = self.id;
|
||||
let client = reqwest::Client::new();
|
||||
*self = client
|
||||
.put(format!("https://fakestoreapi.com/carts/{id}"))
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct ProductInCart {
|
||||
struct ProductInCart {
|
||||
#[serde(rename = "productId")]
|
||||
product_id: usize,
|
||||
quantity: usize,
|
||||
}
|
||||
|
||||
impl ProductInCart {
|
||||
pub async fn fetch_product(&self) -> Result<Product, dioxus::CapturedError> {
|
||||
fetch_product(self.product_id).await
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,23 @@
|
||||
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
|
||||
|
||||
use crate::{
|
||||
api::{fetch_products, Sort},
|
||||
components::nav::Nav,
|
||||
components::product_item::ProductItem,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub(crate) fn Home() -> Element {
|
||||
let products = use_loader(|| fetch_products(10, Sort::Ascending))?;
|
||||
|
||||
rsx! {
|
||||
Nav {}
|
||||
section { class: "p-10",
|
||||
for product in products.iter() {
|
||||
ProductItem {
|
||||
product: product.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/public/loading.css") }
|
||||
SuspenseBoundary {
|
||||
fallback: |_| rsx! { div { class: "spinner", } },
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub fn nav() -> Element {
|
||||
#[component]
|
||||
pub fn Nav() -> Element {
|
||||
rsx! {
|
||||
section { class: "relative",
|
||||
nav { class: "flex justify-between border-b",
|
||||
+1
-1
@@ -3,7 +3,7 @@ use dioxus::prelude::*;
|
||||
use crate::api::Product;
|
||||
|
||||
#[component]
|
||||
pub(crate) fn product_item(product: Product) -> Element {
|
||||
pub(crate) fn ProductItem(product: Product) -> Element {
|
||||
let Product {
|
||||
id,
|
||||
title,
|
||||
+35
-36
@@ -3,44 +3,11 @@ use std::{fmt::Display, str::FromStr};
|
||||
use crate::api::{fetch_product, Product};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
enum Size {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Size::Small => "small".fmt(f),
|
||||
Size::Medium => "medium".fmt(f),
|
||||
Size::Large => "large".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Size {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use Size::*;
|
||||
match s.to_lowercase().as_str() {
|
||||
"small" => Ok(Small),
|
||||
"medium" => Ok(Medium),
|
||||
"large" => Ok(Large),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
pub fn ProductPage(product_id: ReadSignal<usize>) -> Element {
|
||||
let mut quantity = use_signal(|| 1);
|
||||
let mut size = use_signal(Size::default);
|
||||
|
||||
let product = use_server_future(move || fetch_product(product_id()))?;
|
||||
let product = use_loader(move || fetch_product(product_id()))?;
|
||||
|
||||
let Product {
|
||||
title,
|
||||
@@ -50,7 +17,7 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
image,
|
||||
rating,
|
||||
..
|
||||
} = product().unwrap()?;
|
||||
} = product();
|
||||
|
||||
rsx! {
|
||||
section { class: "py-20",
|
||||
@@ -216,6 +183,38 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum Size {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Size::Small => "small".fmt(f),
|
||||
Size::Medium => "medium".fmt(f),
|
||||
Size::Large => "large".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Size {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use Size::*;
|
||||
match s.to_lowercase().as_str() {
|
||||
"small" => Ok(Small),
|
||||
"medium" => Ok(Medium),
|
||||
"large" => Ok(Large),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod icons {
|
||||
use super::*;
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ enum Route {
|
||||
fn Details(product_id: usize) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
components::nav::nav {}
|
||||
components::product_page::product_page {
|
||||
components::nav::Nav {}
|
||||
components::product_page::ProductPage {
|
||||
product_id
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-9
@@ -8,25 +8,19 @@
|
||||
use std::env::current_dir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use dioxus::desktop::{Config, WindowBuilder};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::LaunchBuilder::desktop()
|
||||
.with_cfg(Config::new().with_window(WindowBuilder::new().with_resizable(true)))
|
||||
.launch(app)
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut files = use_signal(Files::new);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
rel: "stylesheet",
|
||||
href: asset!("/assets/fileexplorer.css")
|
||||
}
|
||||
Stylesheet { href: asset!("/assets/fileexplorer.css") }
|
||||
Stylesheet { href: "https://fonts.googleapis.com/icon?family=Material+Icons" }
|
||||
div {
|
||||
document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
|
||||
header {
|
||||
i { class: "material-icons icon-menu", "menu" }
|
||||
h1 { "Files: " {files.read().current()} }
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+54
-109
@@ -13,45 +13,37 @@ use svg_attributes::to;
|
||||
fn main() {
|
||||
LaunchBuilder::new()
|
||||
.with_cfg(server_only! {
|
||||
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
|
||||
})
|
||||
.launch(|| rsx! { Router::<Route> {} });
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/assets/hackernews.css") }
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable)]
|
||||
enum Route {
|
||||
#[redirect("/", || Route::Homepage { story: PreviewState { active_story: None } })]
|
||||
#[route("/:story")]
|
||||
Homepage { story: PreviewState },
|
||||
}
|
||||
|
||||
pub fn App() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
#[route("/story&:story")]
|
||||
StoryPreview { story: Option<i64> },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Homepage(story: ReadSignal<PreviewState>) -> Element {
|
||||
fn StoryPreview(story: ReadSignal<Option<i64>>) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
|
||||
div { display: "flex", flex_direction: "row", width: "100%",
|
||||
div {
|
||||
width: "50%",
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| rsx! {
|
||||
"Loading..."
|
||||
},
|
||||
div { width: "50%",
|
||||
SuspenseBoundary { fallback: |context| rsx! { "Loading..." },
|
||||
Stories {}
|
||||
}
|
||||
}
|
||||
div { width: "50%",
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| rsx! {
|
||||
"Loading preview..."
|
||||
},
|
||||
Preview {
|
||||
story
|
||||
SuspenseBoundary { fallback: |context| rsx! { "Loading preview..." },
|
||||
if let Some(story) = story() {
|
||||
Preview { story_id: story }
|
||||
} else {
|
||||
div { padding: "0.5rem", "Select a story to preview" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,32 +53,31 @@ fn Homepage(story: ReadSignal<PreviewState>) -> Element {
|
||||
|
||||
#[component]
|
||||
fn Stories() -> Element {
|
||||
let stories: Resource<dioxus::Result<Vec<i64>>> = use_server_future(move || async move {
|
||||
let url = format!("{}topstories.json", BASE_API_URL);
|
||||
let mut stories_ids = reqwest::get(&url).await?.json::<Vec<i64>>().await?;
|
||||
stories_ids.truncate(30);
|
||||
Ok(stories_ids)
|
||||
let stories = use_loader(move || async move {
|
||||
let stories_ids = reqwest::get(&format!("{}topstories.json", BASE_API_URL))
|
||||
.await?
|
||||
.json::<Vec<i64>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(30)
|
||||
.collect::<Vec<i64>>();
|
||||
dioxus::Ok(stories_ids)
|
||||
})?;
|
||||
|
||||
match stories().unwrap() {
|
||||
Ok(list) => rsx! {
|
||||
div {
|
||||
for story in list {
|
||||
ChildrenOrLoading {
|
||||
key: "{story}",
|
||||
StoryListing { story }
|
||||
}
|
||||
rsx! {
|
||||
div {
|
||||
for story in stories() {
|
||||
ChildrenOrLoading { key: "{story}",
|
||||
StoryListing { story }
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
let story = use_server_future(move || get_story(story()))?;
|
||||
|
||||
let story = use_loader(move || get_story(story()))?;
|
||||
let StoryItem {
|
||||
title,
|
||||
url,
|
||||
@@ -96,7 +87,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
kids,
|
||||
id,
|
||||
..
|
||||
} = story().unwrap()?.item;
|
||||
} = story().item;
|
||||
|
||||
let url = url.as_deref().unwrap_or_default();
|
||||
let hostname = url
|
||||
@@ -121,7 +112,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
position: "relative",
|
||||
div { font_size: "1.5rem",
|
||||
Link {
|
||||
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
|
||||
to: Route::StoryPreview { story: Some(id) },
|
||||
"{title}"
|
||||
}
|
||||
a {
|
||||
@@ -141,44 +132,9 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PreviewState {
|
||||
active_story: Option<i64>,
|
||||
}
|
||||
|
||||
impl FromStr for PreviewState {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let state = i64::from_str(s)?;
|
||||
Ok(PreviewState {
|
||||
active_story: Some(state),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreviewState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(id) = &self.active_story {
|
||||
write!(f, "{id}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Preview(story: ReadSignal<PreviewState>) -> Element {
|
||||
let PreviewState {
|
||||
active_story: Some(id),
|
||||
} = story()
|
||||
else {
|
||||
return rsx! {"Click on a story to preview it here"};
|
||||
};
|
||||
|
||||
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
|
||||
|
||||
let story = story().unwrap()?;
|
||||
|
||||
fn Preview(story_id: ReadSignal<i64>) -> Element {
|
||||
let story = use_loader(move || get_story(story_id()))?.cloned();
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
|
||||
@@ -194,13 +150,14 @@ fn Preview(story: ReadSignal<PreviewState>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Comment(comment: i64) -> Element {
|
||||
let comment: Resource<dioxus::Result<CommentData>> =
|
||||
use_server_future(use_reactive!(|comment| async move {
|
||||
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment);
|
||||
let mut comment = reqwest::get(&url).await?.json::<CommentData>().await?;
|
||||
Ok(comment)
|
||||
}))?;
|
||||
fn Comment(comment: ReadSignal<i64>) -> Element {
|
||||
let comment = use_loader(move || async move {
|
||||
let mut comment = reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment))
|
||||
.await?
|
||||
.json::<CommentData>()
|
||||
.await?;
|
||||
dioxus::Ok(comment)
|
||||
})?;
|
||||
|
||||
let CommentData {
|
||||
by,
|
||||
@@ -209,7 +166,7 @@ fn Comment(comment: i64) -> Element {
|
||||
id,
|
||||
kids,
|
||||
..
|
||||
} = comment().unwrap()?;
|
||||
} = comment();
|
||||
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
@@ -272,33 +229,21 @@ pub struct StoryItem {
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
pub async fn get_story(id: i64) -> dioxus::Result<StoryPageData> {
|
||||
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id);
|
||||
Ok(reqwest::get(&url).await?.json::<StoryPageData>().await?)
|
||||
pub async fn get_story(id: i64) -> Result<StoryPageData> {
|
||||
Ok(
|
||||
reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, id))
|
||||
.await?
|
||||
.json::<StoryPageData>()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ChildrenOrLoading(children: Element) -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| {
|
||||
rsx! {
|
||||
if let Some(placeholder) = context.suspense_placeholder() {
|
||||
{placeholder}
|
||||
} else {
|
||||
LoadingIndicator {}
|
||||
}
|
||||
}
|
||||
},
|
||||
fallback: |_| rsx! { div { class: "spinner", } },
|
||||
children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn LoadingIndicator() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "spinner",
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -11,9 +11,10 @@ reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
rusqlite = { version = "0.32.0", optional = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = ["web", "server"]
|
||||
web = ["dioxus/web"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
mobile = ["dioxus/mobile"]
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,41 @@
|
||||
use anyhow::Result;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
thread_local! {
|
||||
static DB: std::sync::LazyLock<rusqlite::Connection> = std::sync::LazyLock::new(|| {
|
||||
let conn = rusqlite::Connection::open("hotdogdb/hotdog.db").expect("Failed to open database");
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS dogs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
url TEXT NOT NULL
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
conn
|
||||
});
|
||||
}
|
||||
|
||||
#[get("/api/dogs")]
|
||||
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
|
||||
DB.with(|db| {
|
||||
Ok(db
|
||||
.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
|
||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
|
||||
.collect::<Result<Vec<(usize, String)>, rusqlite::Error>>()?)
|
||||
})
|
||||
}
|
||||
|
||||
#[delete("/api/dogs/{id}")]
|
||||
pub async fn remove_dog(id: usize) -> Result<()> {
|
||||
DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/api/dogs")]
|
||||
pub async fn save_dog(image: String) -> Result<()> {
|
||||
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::{
|
||||
backend::{list_dogs, remove_dog, save_dog},
|
||||
Route,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Favorites() -> Element {
|
||||
let mut favorites = use_loader(list_dogs)?;
|
||||
|
||||
rsx! {
|
||||
div { id: "favorites",
|
||||
for (id , url) in favorites.cloned() {
|
||||
div { class: "favorite-dog", key: "{id}",
|
||||
img { src: "{url}" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
_ = remove_dog(id).await;
|
||||
favorites.restart();
|
||||
},
|
||||
"❌"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn NavBar() -> Element {
|
||||
rsx! {
|
||||
div { id: "title",
|
||||
span {}
|
||||
Link { to: Route::DogView, h1 { "🌭 HotDog! " } }
|
||||
Link { to: Route::Favorites, id: "heart", "♥️" }
|
||||
}
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DogView() -> Element {
|
||||
let mut img_src = use_loader(|| async move {
|
||||
anyhow::Ok(
|
||||
reqwest::get("https://dog.ceo/api/breeds/image/random")
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?["message"]
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
rsx! {
|
||||
div { id: "dogview",
|
||||
img { id: "dogimg", src: "{img_src}" }
|
||||
}
|
||||
div { id: "buttons",
|
||||
button {
|
||||
id: "skip",
|
||||
onclick: move |_| img_src.restart(),
|
||||
"skip"
|
||||
}
|
||||
button {
|
||||
id: "save",
|
||||
onclick: move |_| async move { _ = save_dog(img_src()).await },
|
||||
"save!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
mod backend;
|
||||
mod components;
|
||||
mod frontend;
|
||||
|
||||
use components::{DogView, Favorites, NavBar};
|
||||
use dioxus::prelude::*;
|
||||
use frontend::*;
|
||||
|
||||
#[derive(Routable, PartialEq, Clone)]
|
||||
enum Route {
|
||||
@@ -16,14 +16,14 @@ enum Route {
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(feature = "server"))]
|
||||
server_fn::client::set_server_url("https://hot-dog.fly.dev");
|
||||
dioxus::fullstack::set_server_url("https://hot-dog.fly.dev");
|
||||
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
document::Stylesheet { href: asset!("/assets/main.css") }
|
||||
Stylesheet { href: asset!("/assets/main.css") }
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut api_key = use_signal(|| "".to_string());
|
||||
let mut prompt = use_signal(|| "".to_string());
|
||||
let mut num_images = use_signal(|| 1.to_string());
|
||||
|
||||
let mut image = use_action(move || async move {
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone, Default)]
|
||||
struct ImageResponse {
|
||||
created: i32,
|
||||
data: Vec<UrlImage>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone)]
|
||||
struct UrlImage {
|
||||
url: String,
|
||||
}
|
||||
|
||||
if api_key.peek().is_empty() || prompt.peek().is_empty() || num_images.peek().is_empty() {
|
||||
return dioxus::Ok(ImageResponse::default());
|
||||
}
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.post("https://api.openai.com/v1/images/generations")
|
||||
.json(&serde_json::json!({
|
||||
"prompt": prompt.cloned(),
|
||||
"n": num_images.cloned().parse::<i32>().unwrap_or(1),
|
||||
"size":"1024x1024",
|
||||
}))
|
||||
.bearer_auth(api_key)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ImageResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
});
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
|
||||
div { class: "container",
|
||||
div { class: "columns",
|
||||
div { class: "column",
|
||||
input { class: "input is-primary mt-4",
|
||||
value: "{api_key}",
|
||||
r#type: "text",
|
||||
placeholder: "Your OpenAI API Key",
|
||||
oninput: move |evt| api_key.set(evt.value()),
|
||||
}
|
||||
input { class: "input is-primary mt-4",
|
||||
placeholder: "MAX 1000 Dgts",
|
||||
r#type: "text",
|
||||
value:"{prompt}",
|
||||
oninput: move |evt| prompt.set(evt.value())
|
||||
}
|
||||
input { class: "input is-primary mt-4",
|
||||
r#type: "number",
|
||||
min:"1",
|
||||
max:"10",
|
||||
value:"{num_images}",
|
||||
oninput: move |evt| num_images.set(evt.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "button is-primary",
|
||||
class: if image.is_pending() { "is-loading" },
|
||||
onclick: move |_| {
|
||||
image.call();
|
||||
},
|
||||
"Generate image"
|
||||
}
|
||||
if let Some(image) = image.value() {
|
||||
for image in image.read().data.as_slice() {
|
||||
section { class: "is-flex",
|
||||
div { class: "container is-fluid",
|
||||
div { class: "container has-text-centered",
|
||||
div { class: "is-justify-content-center",
|
||||
div { class: "level",
|
||||
div { class: "level-item",
|
||||
figure { class: "image", img { alt: "", src: "{image.url}", } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -63,7 +63,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
section { class: "todoapp",
|
||||
TodoHeader { todos }
|
||||
section { class: "main",
|
||||
@@ -124,7 +124,7 @@ fn app() -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
section { class: "todoapp",
|
||||
TodoHeader { todos }
|
||||
section { class: "main",
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::{fullstack::Loading, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
@@ -16,10 +17,10 @@ fn app() -> Element {
|
||||
id: 2950159,
|
||||
});
|
||||
|
||||
let current_weather = use_resource(move || async move { get_weather(&country()).await });
|
||||
let current_weather = use_loader(move || get_weather(country()));
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
||||
Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
||||
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
||||
div { class: "flex items-center justify-center flex-row",
|
||||
div { class: "flex items-start justify-center flex-row",
|
||||
@@ -27,14 +28,20 @@ fn app() -> Element {
|
||||
div { class: "flex flex-wrap w-full px-2",
|
||||
div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full dark:bg-gray-600",
|
||||
div { class: "px-6 py-6 relative",
|
||||
if let Some(Ok(weather)) = current_weather.read().as_ref() {
|
||||
CountryData {
|
||||
country: country.read().clone(),
|
||||
weather: weather.clone(),
|
||||
match current_weather {
|
||||
Ok(weather) => rsx! {
|
||||
CountryData {
|
||||
country: country.read().clone(),
|
||||
weather: weather.cloned(),
|
||||
}
|
||||
Forecast { weather: weather.cloned() }
|
||||
},
|
||||
Err(Loading::Pending(_)) => rsx! {
|
||||
div { "Loading weather data..." }
|
||||
},
|
||||
Err(Loading::Failed(_)) => rsx! {
|
||||
div { "Failed to load weather data." }
|
||||
}
|
||||
Forecast { weather: weather.clone() }
|
||||
} else {
|
||||
p { "Loading.." }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,8 +117,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
|
||||
#[component]
|
||||
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
||||
let mut input = use_signal(|| "".to_string());
|
||||
|
||||
let locations = use_resource(move || async move { get_locations(&input()).await });
|
||||
let locations = use_resource(move || get_locations(input()));
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
@@ -197,7 +203,7 @@ struct SearchResponse {
|
||||
results: WeatherLocations,
|
||||
}
|
||||
|
||||
async fn get_locations(input: &str) -> reqwest::Result<WeatherLocations> {
|
||||
async fn get_locations(input: impl Display) -> reqwest::Result<WeatherLocations> {
|
||||
let res = reqwest::get(&format!(
|
||||
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
|
||||
))
|
||||
@@ -226,13 +232,9 @@ struct DailyWeather {
|
||||
temperature_2m_max: Vec<f32>,
|
||||
}
|
||||
|
||||
async fn get_weather(location: &WeatherLocation) -> reqwest::Result<WeatherResponse> {
|
||||
let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
|
||||
.await
|
||||
?
|
||||
async fn get_weather(location: WeatherLocation) -> reqwest::Result<WeatherResponse> {
|
||||
reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
|
||||
.await?
|
||||
.json::<WeatherResponse>()
|
||||
.await
|
||||
?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! A websocket chat demo using Dioxus' built-in websocket support.
|
||||
//!
|
||||
//! We setup an endpoint at `/api/chat` that accepts a `name` and `user_id` query parameter.
|
||||
//! Each client connects to that endpoint, and we use a `tokio::broadcast` channel
|
||||
//! to send messages to all connected clients.
|
||||
//!
|
||||
//! In practice, you'd use a distributed messaging system (Redis PubSub / Kafka / etc) to coordinate
|
||||
//! between multiple server instances and an additional database to persist chat history.
|
||||
|
||||
use dioxus::fullstack::{WebSocketOptions, Websocket, use_websocket};
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
println!("IP and PORT: {:#?}", std::env::var("IP"));
|
||||
println!("IP and PORT: {:#?}", std::env::var("PORT"));
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// store the user's current input
|
||||
let mut input = use_signal(|| "".to_string());
|
||||
|
||||
// Select a unique id for the user, and then use that entropy to pick a random name
|
||||
let user_id = use_signal(uuid::Uuid::new_v4);
|
||||
let user_name = use_signal(|| {
|
||||
match user_id.read().as_bytes()[0] % 7 {
|
||||
0 => "Alice",
|
||||
1 => "Bob",
|
||||
2 => "Eve",
|
||||
3 => "Mallory",
|
||||
4 => "Trent",
|
||||
5 => "Peggy",
|
||||
6 => "Victor",
|
||||
_ => "Charlie",
|
||||
}
|
||||
.to_string()
|
||||
});
|
||||
|
||||
// Store the messages we've received from the server
|
||||
let mut message_list = use_signal(Vec::<ChatMessage>::new);
|
||||
|
||||
// Connect to the websocket endpoint
|
||||
let mut socket =
|
||||
use_websocket(move || uppercase_ws(user_name(), user_id(), Default::default()));
|
||||
|
||||
use_future(move || async move {
|
||||
while let Ok(msg) = socket.recv().await {
|
||||
match msg {
|
||||
ServerEvent::ReceiveMessage(message) => message_list.write().push(message),
|
||||
ServerEvent::Connected { messages } => message_list.set(messages),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
h1 { "WebSocket Chat" }
|
||||
p { "Connection status: {socket.status():?} as {user_name}" }
|
||||
input {
|
||||
placeholder: "Type a message",
|
||||
value: "{input}",
|
||||
oninput: move |e| async move { input.set(e.value()) },
|
||||
onkeydown: move |e| async move {
|
||||
if e.key() == Key::Enter {
|
||||
_ = socket.send(ClientEvent::SendMessage(input.read().clone())).await;
|
||||
input.set("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
for message in message_list.read().iter().rev() {
|
||||
pre { "{message.name}: {message.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The events that the client can send to the server
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ClientEvent {
|
||||
SendMessage(String),
|
||||
}
|
||||
|
||||
/// The events that the server can send to the client
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ServerEvent {
|
||||
Connected { messages: Vec<ChatMessage> },
|
||||
ReceiveMessage(ChatMessage),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct ChatMessage {
|
||||
user_id: Uuid,
|
||||
name: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/api/chat?name&user_id")]
|
||||
async fn uppercase_ws(
|
||||
name: String,
|
||||
user_id: Uuid,
|
||||
options: WebSocketOptions,
|
||||
) -> Result<Websocket<ClientEvent, ServerEvent>> {
|
||||
use std::sync::LazyLock;
|
||||
use tokio::sync::{
|
||||
Mutex,
|
||||
broadcast::{self, Sender},
|
||||
};
|
||||
|
||||
// Every chat app needs a chat room! For this demo, we just use a tokio broadcast channel and a mutex-protected
|
||||
// list of messages to store chat history.
|
||||
//
|
||||
// We place these types in the body of this serverfn since they're not used on the client, only the server.
|
||||
static MESSAGES: LazyLock<Mutex<Vec<ChatMessage>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
||||
static BROADCAST: LazyLock<Sender<ChatMessage>> = LazyLock::new(|| broadcast::channel(100).0);
|
||||
|
||||
Ok(options.on_upgrade(move |mut socket| async move {
|
||||
// Send back all the messages from the room to the new client
|
||||
let messages = MESSAGES.lock().await.clone();
|
||||
_ = socket.send(ServerEvent::Connected { messages }).await;
|
||||
|
||||
// Subscriber to the broadcast channel
|
||||
let sender = BROADCAST.clone();
|
||||
let mut broadcast = sender.subscribe();
|
||||
|
||||
// Announce that we've joined
|
||||
let _ = sender.send(ChatMessage {
|
||||
message: format!("{name} has connected."),
|
||||
user_id,
|
||||
name: "[CONSOLE]".to_string(),
|
||||
});
|
||||
|
||||
// Loop poll the broadcast receiver and the websocket for new messages
|
||||
// If we receive a message from the broadcast channel, send it to the client
|
||||
// If we receive a message from the client, broadcast it to all other clients and save it to the message list
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok(msg) = broadcast.recv() => {
|
||||
let _ = socket.send(ServerEvent::ReceiveMessage(msg)).await;
|
||||
}
|
||||
Ok(ClientEvent::SendMessage(message)) = socket.recv() => {
|
||||
let chat_message = ChatMessage {
|
||||
user_id,
|
||||
name: name.clone(),
|
||||
message,
|
||||
};
|
||||
let _ = sender.send(chat_message.clone());
|
||||
MESSAGES.lock().await.push(chat_message.clone());
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
_ = sender.send(ChatMessage {
|
||||
name: "[CONSOLE]".to_string(),
|
||||
message: format!("{name} has disconnected."),
|
||||
user_id,
|
||||
});
|
||||
}))
|
||||
}
|
||||
@@ -12,8 +12,9 @@ fn app() -> Element {
|
||||
let mut disabled = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
div { style: "text-align: center; margin: 20px; display: flex; flex-direction: column; align-items: center;",
|
||||
button { onclick: move |_| disabled.toggle(),
|
||||
div { text_align: "center", margin: "20px", display: "flex", flex_direction: "column", align_items: "center",
|
||||
button {
|
||||
onclick: move |_| disabled.toggle(),
|
||||
"click to "
|
||||
if disabled() { "enable" } else { "disable" }
|
||||
" the lower button"
|
||||
File renamed without changes.
@@ -7,7 +7,7 @@
|
||||
//! If you `go-to-definition` on the `svg` element, you'll see its custom namespace.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use rand::{rng, Rng};
|
||||
use rand::{Rng, rng};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
@@ -45,7 +45,7 @@ fn Dice() -> Element {
|
||||
value.set(rng().random_range(1..=6))
|
||||
},
|
||||
rect { x: -1000, y: -1000, width: 2000, height: 2000, rx: 200, fill: "#aaa" }
|
||||
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, &active)| active) {
|
||||
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, active)| **active) {
|
||||
circle {
|
||||
cx: *x * 600,
|
||||
cy: *y * 600,
|
||||
File renamed without changes.
@@ -20,11 +20,11 @@ fn app() -> Element {
|
||||
return;
|
||||
}
|
||||
|
||||
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
|
||||
response.respond(Response::new(include_bytes!("../assets/logo.png").to_vec()));
|
||||
});
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
h1 { "Dynamic Assets" }
|
||||
img { src: "/logos/logo.png" }
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
//! This example shows how to add metadata to the page with the Meta component
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
// You can use the Meta component to render a meta tag into the head of the page
|
||||
// Meta tags are useful to provide information about the page to search engines and social media sites
|
||||
// This example sets up meta tags for the open graph protocol for social media previews
|
||||
Meta { property: "og:title", content: "My Site" }
|
||||
Meta { property: "og:type", content: "website" }
|
||||
Meta { property: "og:url", content: "https://www.example.com" }
|
||||
Meta { property: "og:image", content: "https://example.com/image.jpg" }
|
||||
Meta { name: "description", content: "My Site is a site" }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ fn app() -> Element {
|
||||
use_context_provider(|| Signal::new(Theme::Light));
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
main {
|
||||
class: "main-container",
|
||||
|
||||
@@ -54,9 +54,20 @@ fn Show() -> Element {
|
||||
ErrorBoundary {
|
||||
handle_error: |errors: ErrorContext| {
|
||||
rsx! {
|
||||
for error in errors.errors() {
|
||||
if let Some(error) = error.show() {
|
||||
{error}
|
||||
for error in errors.error() {
|
||||
// You can downcast the error to see if it's a specific type and render something specific for it
|
||||
if let Some(_error) = error.downcast_ref::<std::num::ParseIntError>() {
|
||||
div {
|
||||
background_color: "red",
|
||||
border: "black",
|
||||
border_width: "2px",
|
||||
border_radius: "5px",
|
||||
p { "Failed to parse data" }
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Go back to the homepage"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pre {
|
||||
color: "red",
|
||||
@@ -79,24 +90,8 @@ fn ParseNumberWithShow() -> Element {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let request_data = "0.5";
|
||||
let data: i32 = request_data.parse()
|
||||
// You can attach rsx to results that can be displayed in the Error Boundary
|
||||
.show(|_| rsx! {
|
||||
div {
|
||||
background_color: "red",
|
||||
border: "black",
|
||||
border_width: "2px",
|
||||
border_radius: "5px",
|
||||
p { "Failed to parse data" }
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Go back to the homepage"
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let data: i32 = request_data.parse()?;
|
||||
println!("parsed {data}");
|
||||
|
||||
Ok(())
|
||||
},
|
||||
"Click to throw an error"
|
||||
@@ -18,7 +18,7 @@ fn main() {
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Increment {}
|
||||
Decrement {}
|
||||
Reset {}
|
||||
File renamed without changes.
@@ -17,7 +17,7 @@ fn app() -> Element {
|
||||
let mut state = use_signal(|| PlayerState { is_playing: false });
|
||||
|
||||
rsx!(
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
h1 {"Select an option"}
|
||||
|
||||
// Add some cute animations if the radio is playing!
|
||||
@@ -68,7 +68,7 @@ fn app() -> Element {
|
||||
}
|
||||
|
||||
// We can also use the signal value as a slice
|
||||
if let [ref first, .., ref last] = saved_values.read().as_slice() {
|
||||
if let [first, .., last] = saved_values.read().as_slice() {
|
||||
li { "First and last: {first}, {last}" }
|
||||
} else {
|
||||
"No saved values"
|
||||
File renamed without changes.
@@ -12,7 +12,7 @@ fn app() -> Element {
|
||||
let mut millis = use_signal(|| 0);
|
||||
|
||||
use_future(move || async move {
|
||||
// Save our initial timea
|
||||
// Save our initial time
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
|
||||
use async_std::task::sleep;
|
||||
use dioxus::prelude::*;
|
||||
use futures_util::{future, stream, Stream, StreamExt};
|
||||
use futures_util::{Stream, StreamExt, future, stream};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Suspense in Dioxus
|
||||
//!
|
||||
//! Suspense allows components to bubble up loading states to parent components, simplifying data fetching.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
h1 { "Dogs are very important" }
|
||||
p {
|
||||
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
|
||||
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
|
||||
"The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the"
|
||||
"dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]"
|
||||
"by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]"
|
||||
}
|
||||
h3 { "Illustrious Dog Photo" }
|
||||
ErrorBoundary { handle_error: |_| rsx! { p { "Error loading doggos" } },
|
||||
SuspenseBoundary { fallback: move |_| rsx! { "Loading doggos..." },
|
||||
Doggo {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Doggo() -> Element {
|
||||
// `use_loader` returns a Result<Loader<T>, Loading>. Loading can either be "Pending" or "Failed".
|
||||
// When we use the `?` operator, the pending/error state will be thrown to the nearest Suspense or Error boundary.
|
||||
//
|
||||
// During SSR, `use_loader` will serialize the contents of the fetch, and during hydration, the client will
|
||||
// use the pre-fetched data instead of re-fetching to render.
|
||||
let mut dog = use_loader(move || async move {
|
||||
#[derive(serde::Deserialize, serde::Serialize, PartialEq)]
|
||||
struct DogApi {
|
||||
message: String,
|
||||
}
|
||||
|
||||
reqwest::get("https://dog.ceo/api/breeds/image/random/")
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<DogApi>()
|
||||
.await
|
||||
})?;
|
||||
|
||||
rsx! {
|
||||
button { onclick: move |_| dog.restart(), "Click to fetch another doggo" }
|
||||
div {
|
||||
img {
|
||||
max_width: "500px",
|
||||
max_height: "500px",
|
||||
src: "{dog.read().message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const STYLE: Asset = asset!("/examples/assets/flat_router.css");
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
}
|
||||
})
|
||||
File renamed without changes.
@@ -16,7 +16,7 @@ fn main() {
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! (
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -13,7 +13,7 @@ const STYLE: Asset = asset!("/examples/assets/router.css");
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
Stylesheet { href: STYLE }
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "fullstack-auth-example-with-fullstack"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
axum = { workspace = true, optional = true, features = ["macros"]}
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
async-trait = { version = "0.1.88", optional = true }
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"_unstable-all-types",
|
||||
"tls-native-tls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
tower = { workspace = true, optional = true }
|
||||
|
||||
execute = "0.2.13"
|
||||
serde = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dependencies.axum_session]
|
||||
workspace = true
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
workspace = true
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_sqlx]
|
||||
workspace = true
|
||||
features = ["sqlite"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["web", "server"]
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"dep:tower-http",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:axum_session",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session_sqlx",
|
||||
"dep:http",
|
||||
"dep:tower",
|
||||
]
|
||||
web = ["dioxus/web", "dep:dioxus-web"]
|
||||
@@ -0,0 +1,81 @@
|
||||
//! The code here is pulled from the `axum-session-auth` crate examples, requiring little to no
|
||||
//! modification to work with dioxus fullstack.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum_session_auth::*;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) type Session = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
pub(crate) type AuthLayer =
|
||||
axum_session_auth::AuthSessionLayer<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub(crate) struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let db = pool.unwrap();
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
struct SqlUser {
|
||||
id: i32,
|
||||
anonymous: bool,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(userid)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = $1;",
|
||||
)
|
||||
.bind(userid)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(User {
|
||||
id: sqluser.id,
|
||||
anonymous: sqluser.anonymous,
|
||||
username: sqluser.username,
|
||||
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
!self.anonymous
|
||||
}
|
||||
|
||||
fn is_anonymous(&self) -> bool {
|
||||
self.anonymous
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HasPermission<SqlitePool> for User {
|
||||
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
|
||||
self.permissions.contains(perm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! This example showcases how to use the `axum-session-auth` crate with Dioxus fullstack.
|
||||
//! We add the `auth::Session` extractor to our server functions to get access to the current user session.
|
||||
//!
|
||||
//! To initialize the axum router, we use `dioxus::serve` to spawn a custom axum server that creates
|
||||
//! our database, session store, and authentication layer.
|
||||
//!
|
||||
//! The `.serve_dioxus_application` method is used to mount our Dioxus app as a fallback service to
|
||||
//! handle HTML rendering and static assets.
|
||||
//!
|
||||
//! We easily share the "permissions" between the server and client by using a `HashSet<String>`
|
||||
//! which is serialized to/from JSON automatically by the server function system.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
fn main() {
|
||||
// On the client, we simply launch the app as normal, taking over the main thread
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(app);
|
||||
|
||||
// On the server, we can use `dioxus::serve` to create a server that serves our app.
|
||||
//
|
||||
// The `serve` function takes a closure that returns a `Future` which resolves to an `axum::Router`.
|
||||
//
|
||||
// We return a `Router` such that dioxus sets up logging, hot-reloading, devtools, and wires up the
|
||||
// IP and PORT environment variables to our server.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async {
|
||||
use crate::auth::*;
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use sqlx::{sqlite::SqlitePoolOptions, Executor};
|
||||
|
||||
// Create an in-memory SQLite database and set up our tables
|
||||
let db = SqlitePoolOptions::new()
|
||||
.max_connections(20)
|
||||
.connect_with("sqlite::memory:".parse()?)
|
||||
.await?;
|
||||
|
||||
// Create the tables (sessions, users)
|
||||
db.execute(r#"CREATE TABLE IF NOT EXISTS users ( "id" INTEGER PRIMARY KEY, "anonymous" BOOLEAN NOT NULL, "username" VARCHAR(256) NOT NULL )"#,)
|
||||
.await?;
|
||||
db.execute(r#"CREATE TABLE IF NOT EXISTS user_permissions ( "user_id" INTEGER NOT NULL, "token" VARCHAR(256) NOT NULL)"#,)
|
||||
.await?;
|
||||
|
||||
// Insert in some test data for two users (one anonymous, one normal)
|
||||
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 1, true, 'Guest' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
|
||||
.await?;
|
||||
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 2, false, 'Test' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
|
||||
.await?;
|
||||
|
||||
// Make sure our test user has the ability to view categories
|
||||
db.execute(r#"INSERT INTO user_permissions (user_id, token) SELECT 2, 'Category::View'"#)
|
||||
.await?;
|
||||
|
||||
// Create an axum router that dioxus will attach the app to
|
||||
Ok(dioxus::server::router(app)
|
||||
.layer(
|
||||
AuthLayer::new(Some(db.clone()))
|
||||
.with_config(AuthConfig::<i64>::default().with_anonymous_user_id(Some(1))),
|
||||
)
|
||||
.layer(SessionLayer::new(
|
||||
SessionStore::<SessionSqlitePool>::new(
|
||||
Some(db.into()),
|
||||
SessionConfig::default().with_table_name("test_table"),
|
||||
)
|
||||
.await?,
|
||||
)))
|
||||
});
|
||||
}
|
||||
|
||||
/// The UI for our app - is just a few buttons to call our server functions and display the results.
|
||||
fn app() -> Element {
|
||||
let mut login = use_action(login);
|
||||
let mut user_name = use_action(get_user_name);
|
||||
let mut permissions = use_action(get_permissions);
|
||||
let mut logout = use_action(logout);
|
||||
|
||||
let fetch_new = move |_| async move {
|
||||
user_name.call().await;
|
||||
permissions.call().await;
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
login.call().await;
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
logout.call().await;
|
||||
},
|
||||
"Logout"
|
||||
}
|
||||
button {
|
||||
onclick: fetch_new,
|
||||
"Fetch User Info"
|
||||
}
|
||||
|
||||
pre { "Logged in: {login.result():?}" }
|
||||
pre { "User name: {user_name.result():?}" }
|
||||
pre { "Permissions: {permissions.result():?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We use the `auth::Session` extractor to get access to the current user session.
|
||||
/// This lets us modify the user session, log in/out, and access the current user.
|
||||
#[post("/api/user/login", auth: auth::Session)]
|
||||
pub async fn login() -> Result<()> {
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Just like `login`, but this time we log out the user.
|
||||
#[post("/api/user/logout", auth: auth::Session)]
|
||||
pub async fn logout() -> Result<()> {
|
||||
auth.logout_user();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We can access the current user via `auth.current_user`.
|
||||
/// We can have both anonymous user (id 1) and a logged in user (id 2).
|
||||
///
|
||||
/// Logged-in users will have more permissions which we can modify.
|
||||
#[post("/api/user/name", auth: auth::Session)]
|
||||
pub async fn get_user_name() -> Result<String> {
|
||||
Ok(auth.current_user.unwrap().username)
|
||||
}
|
||||
|
||||
/// Get the current user's permissions, guarding the endpoint with the `Auth` validator.
|
||||
/// If this returns false, we use the `or_unauthorized` extension to return a 401 error.
|
||||
#[get("/api/user/permissions", auth: auth::Session)]
|
||||
pub async fn get_permissions() -> Result<HashSet<String>> {
|
||||
use crate::auth::User;
|
||||
use axum_session_auth::{Auth, Rights};
|
||||
|
||||
let user = auth.current_user.unwrap();
|
||||
|
||||
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
|
||||
.requires(Rights::any([
|
||||
Rights::permission("Category::View"),
|
||||
Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(&user, &axum::http::Method::GET, None)
|
||||
.await
|
||||
.or_unauthorized("You do not have permission to view categories")?;
|
||||
|
||||
Ok(user.permissions)
|
||||
}
|
||||
@@ -10,8 +10,6 @@ publish = false
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true, optional = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
@@ -49,7 +47,6 @@ optional = true
|
||||
default = []
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dep:dioxus-cli-config",
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"dep:tower-http",
|
||||
@@ -1,23 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
http::Method,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::*;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use core::pin::Pin;
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) type Session =
|
||||
axum_session_auth::AuthSession<User, i64, SessionSqlitePool, sqlx::SqlitePool>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub(crate) struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
@@ -25,25 +17,10 @@ pub struct User {
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlPermissionTokens {
|
||||
pub(crate) struct SqlPermissionTokens {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
let mut permissions = HashSet::new();
|
||||
|
||||
permissions.insert("Category::View".to_owned());
|
||||
|
||||
Self {
|
||||
id: 1,
|
||||
anonymous: true,
|
||||
username: "Guest".into(),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
@@ -76,6 +53,13 @@ impl HasPermission<SqlitePool> for User {
|
||||
|
||||
impl User {
|
||||
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
struct SqlUser {
|
||||
id: i32,
|
||||
anonymous: bool,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
@@ -91,10 +75,15 @@ impl User {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
Some(User {
|
||||
id: sqluser.id,
|
||||
anonymous: sqluser.anonymous,
|
||||
username: sqluser.username,
|
||||
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_user_tables(pool: &SqlitePool) {
|
||||
pub async fn create_user_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -105,20 +94,17 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -130,8 +116,7 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -143,8 +128,7 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -153,51 +137,8 @@ impl User {
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub struct SqlUser {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
anonymous: self.anonymous,
|
||||
username: self.username,
|
||||
permissions: if let Some(user_perms) = sql_user_perms {
|
||||
user_perms
|
||||
.into_iter()
|
||||
.map(|x| x.token)
|
||||
.collect::<HashSet<String>>()
|
||||
} else {
|
||||
HashSet::<String>::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_to_database() -> SqlitePool {
|
||||
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
|
||||
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(connect_opts)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub type Session =
|
||||
axum_session_auth::AuthSession<crate::auth::User, i64, SessionSqlitePool, sqlx::SqlitePool>;
|
||||
|
||||
pub async fn get_session() -> ServerFnResult<Session> {
|
||||
extract::<Session, _>()
|
||||
.await
|
||||
.map_err(|_| ServerFnError::new("AuthSessionLayer was not found"))
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async {
|
||||
use crate::auth::*;
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use std::str::FromStr;
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(SqliteConnectOptions::from_str("sqlite::memory:").unwrap())
|
||||
.await?;
|
||||
|
||||
// This Defaults as normal Cookies.
|
||||
// To enable Private cookies for integrity, and authenticity please check the next Example.
|
||||
let session_config = SessionConfig::default().with_table_name("test_table");
|
||||
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
|
||||
let session_store =
|
||||
SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
User::create_user_tables(&pool).await?;
|
||||
|
||||
// build our application with some routes
|
||||
Ok(
|
||||
dioxus::server::router(app)
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::new(Some(pool))
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store)),
|
||||
)
|
||||
});
|
||||
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut user_name = use_action(get_user_name);
|
||||
let mut permissions = use_action(get_permissions);
|
||||
let mut login = use_action(login);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
login.call();
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
user_name.call();
|
||||
},
|
||||
"Get User Name"
|
||||
}
|
||||
"User name: {user_name.value().unwrap_or_default()}"
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
permissions.call();
|
||||
},
|
||||
"Get Permissions"
|
||||
}
|
||||
"Permissions: {permissions.value().unwrap_or_default()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/user/name", auth: auth::Session)]
|
||||
pub async fn get_user_name() -> Result<String> {
|
||||
Ok(auth
|
||||
.current_user
|
||||
.context("UNAUTHORIZED")?
|
||||
.username
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[post("/api/user/login", auth: auth::Session)]
|
||||
pub async fn login() -> Result<()> {
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/user/permissions", auth: auth::Session)]
|
||||
pub async fn get_permissions() -> Result<String> {
|
||||
use crate::auth::User;
|
||||
use axum_session_auth::{Auth, Rights};
|
||||
|
||||
let user = auth.current_user.as_ref().context("UNAUTHORIZED")?;
|
||||
|
||||
// lets check permissions only and not worry about if they are anon or not
|
||||
let has_permissions =
|
||||
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::POST], false)
|
||||
.requires(Rights::any([
|
||||
Rights::permission("Category::View"),
|
||||
Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(user, &axum::http::Method::GET, None)
|
||||
.await;
|
||||
|
||||
if !has_permissions {
|
||||
dioxus::core::bail!(
|
||||
"User {}, Does not have permissions needed to view this page please login",
|
||||
user.username
|
||||
);
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"User has Permissions needed. {:?}",
|
||||
user.permissions
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! This example demonstrates how to use `dioxus::serve` with a custom Axum router.
|
||||
//!
|
||||
//! By default, `dioxus::launch` takes over the main thread and runs the Dioxus application.
|
||||
//! However, if you want to integrate Dioxus into an existing web server or use a custom router,
|
||||
//! you can use `dioxus::serve` to create a server that serves your Dioxus application alongside
|
||||
//! other routes.
|
||||
//!
|
||||
//! `dioxus::serve` sets up an async runtime, logging, hot-reloading, crash handling, and more.
|
||||
//! You can then use the `.serve_dioxus_application` method on your router to serve the Dioxus app.
|
||||
//!
|
||||
//! `dioxus::serve` is most useful for customizing the server setup, such as adding middleware,
|
||||
//! custom routes, or integrating with existing axum backend code.
|
||||
//!
|
||||
//! Note that `dioxus::serve` is accepts a Router from `axum`. Dioxus will use the IP and PORT
|
||||
//! environment variables to determine where to bind the server. To customize the port, use environment
|
||||
//! variables or a `.env` file.
|
||||
//!
|
||||
//! On other platforms (like desktop or mobile), you'll want to use `dioxus::launch` instead and then
|
||||
//! handle async loading of data through hooks like `use_future` or `use_resource` and give the user
|
||||
//! a loading state while data is being fetched.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// On the client we just launch the app as normal.
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(app);
|
||||
|
||||
// On the server, we can use `dioxus::serve` and `.serve_dioxus_application` to serve our app with routing.
|
||||
// The `dioxus::server::router` function creates a new axum Router with the necessary routes to serve the Dioxus app.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async move {
|
||||
use dioxus::server::axum::routing::{get, post};
|
||||
|
||||
Ok(dioxus::server::router(app)
|
||||
.route("/submit", post(|| async { "Form submitted!" }))
|
||||
.route("/about", get(|| async { "About us" }))
|
||||
.route("/contact", get(|| async { "Contact us" })))
|
||||
});
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div { "Hello from Dioxus!" }
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -2,9 +2,10 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// Set the url of the server where server functions are hosted.
|
||||
// Make sure to set the url of the server where server functions are hosted - they aren't always at localhost
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::fullstack::set_server_url("http://127.0.0.1:8080");
|
||||
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
@@ -30,14 +31,13 @@ pub fn app() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[post("/api/data")]
|
||||
async fn post_server_data(data: String) -> ServerFnResult {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[get("/api/data")]
|
||||
async fn get_server_data() -> ServerFnResult<String> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! This example showcases a fullstack variant of the "dog app" demo, but with the loader and actions
|
||||
//! self-hosted instead of using the Dog API.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
|
||||
let breed_list = use_loader(list_breeds)?;
|
||||
|
||||
// Whenever this action is called, it will re-run the future and return the result.
|
||||
let mut breed = use_action(get_random_breed_image);
|
||||
|
||||
rsx! {
|
||||
h1 { "Doggo selector" }
|
||||
div { width: "400px",
|
||||
for cur_breed in breed_list.read().iter().take(20).cloned() {
|
||||
button { onclick: move |_| { breed.call(cur_breed.clone()); }, "{cur_breed}" }
|
||||
}
|
||||
}
|
||||
div {
|
||||
match breed.result() {
|
||||
None => rsx! { div { "Click the button to fetch a dog!" } },
|
||||
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
||||
Some(Ok(res)) => rsx! { img { max_width: "500px", max_height: "500px", src: "{res}" } },
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/breeds/list/all")]
|
||||
async fn list_breeds() -> Result<Vec<String>> {
|
||||
Ok(vec!["bulldog".into(), "labrador".into(), "poodle".into()])
|
||||
}
|
||||
|
||||
#[get("/api/breed/{breed}/images/random")]
|
||||
async fn get_random_breed_image(breed: String) -> Result<String> {
|
||||
match breed.as_str() {
|
||||
"bulldog" => Ok("https://images.dog.ceo/breeds/buhund-norwegian/hakon3.jpg".into()),
|
||||
"labrador" => Ok("https://images.dog.ceo/breeds/labrador/n02099712_2501.jpg".into()),
|
||||
"poodle" => Ok("https://images.dog.ceo/breeds/poodle-standard/n02113799_5973.jpg".into()),
|
||||
_ => HttpError::not_found("Breed not found")?,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! This example shows how to get access to the full axum request in a handler.
|
||||
//!
|
||||
//! The extra arguments in the `post` macro are passed to the handler function, but not exposed
|
||||
//! to the client. This means we can still call the endpoint from the client, but have full access
|
||||
//! to the request on the server.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut file_id = use_action(full_request);
|
||||
|
||||
rsx! {
|
||||
div { "Access to full axum request" }
|
||||
button { onclick: move |_| file_id.call(), "Upload file" }
|
||||
}
|
||||
}
|
||||
|
||||
/// Example of accessing the full axum request in a handler
|
||||
///
|
||||
/// The `request: axum_core::extract::Request` argument is placed in the handler function, but not
|
||||
/// exposed to the client.
|
||||
#[post("/api/full_request_access", request: axum_core::extract::Request)]
|
||||
async fn full_request() -> Result<()> {
|
||||
let headers = request.headers();
|
||||
|
||||
if headers.contains_key("x-api-key") {
|
||||
println!("API key found");
|
||||
} else {
|
||||
println!("No API key found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! A simple example using Dioxus Fullstack to call a server action.
|
||||
//!
|
||||
//! the `get`, `post`, `put`, `delete`, etc macros are used to define server actions that can be
|
||||
//! called from the client. The action can take arguments and return a value, and the client
|
||||
//! will automatically serialize and deserialize the data.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
let mut message = use_action(get_message);
|
||||
|
||||
rsx! {
|
||||
h1 { "Server says: "}
|
||||
pre { "{message:?}"}
|
||||
button { onclick: move |_| message.call("world".into(), 30), "Click me!" }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[get("/api/{name}/?age")]
|
||||
async fn get_message(name: String, age: i32) -> Result<String> {
|
||||
Ok(format!("Hello {}, you are {} years old!", name, age))
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//! An example of handling errors from server functions.
|
||||
//!
|
||||
//! This example showcases a few important error handling patterns when using Dioxus Fullstack.
|
||||
//!
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --web
|
||||
//! ```
|
||||
//!
|
||||
//! What this example shows:
|
||||
//! - You can return `anyhow::Result<T>` (i.e. `Result<T>` without an `E`) for
|
||||
//! untyped errors with rich context (converted to HTTP 500 responses by default).
|
||||
//! - You can return `Result<T, E>` where `E` is one of:
|
||||
//! - `HttpError` (convenience for returning HTTP status codes)
|
||||
//! - `StatusCode` (return raw status codes)
|
||||
//! - a custom error type that implements `From<ServerFnError>` or
|
||||
//! is `Serialize`/`Deserialize` so it can be sent to the client.
|
||||
//! - This file demonstrates external API errors, custom typed errors, explicit
|
||||
//! HTTP errors, and basic success cases. The UI uses `use_action` to call
|
||||
//! server functions and shows loading/result states simply.
|
||||
//!
|
||||
//! Try running requests against the endpoints directly with `curl` or `postman` to see the actual HTTP responses!
|
||||
|
||||
use dioxus::fullstack::{Json, StatusCode};
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(|| {
|
||||
let mut dog_data = use_action(get_dog_data);
|
||||
let mut dog_data_err = use_action(get_dog_data_err);
|
||||
let mut ip_data = use_action(get_ip_data);
|
||||
let mut custom_data = use_action(move || {
|
||||
get_custom_encoding(Json(serde_json::json!({
|
||||
"example": "data",
|
||||
"number": 123,
|
||||
"array": [1, 2, 3],
|
||||
})))
|
||||
});
|
||||
let mut error_data = use_action(get_throws_error);
|
||||
let mut typed_error_data = use_action(get_throws_typed_error);
|
||||
let mut throws_ok_data = use_action(get_throws_ok);
|
||||
let mut http_error_data = use_action(throws_http_error);
|
||||
let mut http_error_context_data = use_action(throws_http_error_context);
|
||||
|
||||
rsx! {
|
||||
button { onclick: move |_| { dog_data.call(); }, "Fetch dog data" }
|
||||
button { onclick: move |_| { ip_data.call(); }, "Fetch IP data" }
|
||||
button { onclick: move |_| { custom_data.call(); }, "Fetch custom encoded data" }
|
||||
button { onclick: move |_| { error_data.call(); }, "Fetch error data" }
|
||||
button { onclick: move |_| { typed_error_data.call(); }, "Fetch typed error data" }
|
||||
button { onclick: move |_| { dog_data_err.call(); }, "Fetch dog error data" }
|
||||
button { onclick: move |_| { throws_ok_data.call(); }, "Fetch throws ok data" }
|
||||
button { onclick: move |_| { http_error_data.call(); }, "Fetch HTTP 400" }
|
||||
button { onclick: move |_| { http_error_context_data.call(); }, "Fetch HTTP 400 (context)" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
ip_data.reset();
|
||||
dog_data.reset();
|
||||
custom_data.reset();
|
||||
error_data.reset();
|
||||
typed_error_data.reset();
|
||||
dog_data_err.reset();
|
||||
throws_ok_data.reset();
|
||||
http_error_data.reset();
|
||||
http_error_context_data.reset();
|
||||
},
|
||||
"Clear data"
|
||||
}
|
||||
div { display: "flex", flex_direction: "column", gap: "8px",
|
||||
pre { "Dog data: {dog_data.value():#?}" }
|
||||
pre { "IP data: {ip_data.value():#?}" }
|
||||
pre { "Custom encoded data: {custom_data.value():#?}" }
|
||||
pre { "Error data: {error_data.result():#?}" }
|
||||
pre { "Typed error data: {typed_error_data.result():#?}" }
|
||||
pre { "HTTP 400 data: {http_error_data.result():#?}" }
|
||||
pre { "HTTP 400 (context) data: {http_error_context_data.result():#?}" }
|
||||
pre { "Dog error data: {dog_data_err.result():#?}" }
|
||||
pre { "Throws ok data: {throws_ok_data.result():#?}" }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Simple POST endpoint used to show a successful server function that returns `StatusCode`.
|
||||
#[post("/api/data")]
|
||||
async fn post_server_data(data: String) -> Result<(), StatusCode> {
|
||||
println!("Server received: {}", data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches IP info from an external service. Demonstrates propagation of external errors.
|
||||
#[get("/api/ip-data")]
|
||||
async fn get_ip_data() -> Result<serde_json::Value> {
|
||||
Ok(reqwest::get("https://httpbin.org/ip").await?.json().await?)
|
||||
}
|
||||
|
||||
/// Fetches a random dog image (successful external API example).
|
||||
#[get("/api/dog-data")]
|
||||
async fn get_dog_data() -> Result<serde_json::Value> {
|
||||
Ok(reqwest::get("https://dog.ceo/api/breeds/image/random")
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Calls the Dog API with an invalid breed to trigger an external API error (e.g. 404).
|
||||
#[get("/api/dog-data-err")]
|
||||
async fn get_dog_data_err() -> Result<serde_json::Value> {
|
||||
Ok(
|
||||
reqwest::get("https://dog.ceo/api/breed/NOT_A_REAL_DOG/images")
|
||||
.await?
|
||||
.json()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
/// Accepts JSON and returns a custom-encoded JSON response.
|
||||
#[post("/api/custom-encoding")]
|
||||
async fn get_custom_encoding(takes: Json<serde_json::Value>) -> Result<serde_json::Value> {
|
||||
Ok(serde_json::json!({
|
||||
"message": "This response was encoded with a custom encoder!",
|
||||
"success": true,
|
||||
"you sent": takes.0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns an untyped `anyhow` error with context (results in HTTP 500).
|
||||
#[get("/api/untyped-error")]
|
||||
async fn get_throws_error() -> Result<()> {
|
||||
Err(None.context("This is an example error using anyhow::Error")?)
|
||||
}
|
||||
|
||||
/// Demonstrates returning an explicit HTTP error (400 Bad Request) using `HttpError`.
|
||||
#[get("/api/throws-http-error")]
|
||||
async fn throws_http_error() -> Result<()> {
|
||||
HttpError::bad_request("Bad request example")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convenience example: handles an Option and returns HTTP 400 with a message if None.
|
||||
#[get("/api/throws-http-error-context")]
|
||||
async fn throws_http_error_context() -> Result<String> {
|
||||
let res = None.or_bad_request("Value was None")?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// A simple server function that always succeeds.
|
||||
#[get("/api/throws-ok")]
|
||||
async fn get_throws_ok() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Serialize, Deserialize)]
|
||||
enum MyCustomError {
|
||||
#[error("bad request")]
|
||||
BadRequest { custom_name: String },
|
||||
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("internal server error: {0}")]
|
||||
ServerFnError(#[from] ServerFnError),
|
||||
}
|
||||
|
||||
/// Returns a custom typed error (serializable) so clients can handle specific cases.
|
||||
#[get("/api/typed-error")]
|
||||
async fn get_throws_typed_error() -> Result<(), MyCustomError> {
|
||||
Err(MyCustomError::BadRequest {
|
||||
custom_name: "Invalid input".into(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! This example shows how you can extract a HeaderMap from requests to read custom headers.
|
||||
//!
|
||||
//! The extra arguments in the `#[get(...)]` macro are passed to the underlying axum handler,
|
||||
//! and only visible on the server. This lets you run normal axum extractors like `HeaderMap`,
|
||||
//! `TypedHeader`, `Query`, etc.
|
||||
//!
|
||||
//! Note that headers returned by the server are not always visible to the client due to CORS.
|
||||
//! Headers like `Set-Cookie` are hidden by default, and need to be explicitly allowed
|
||||
//! by the server using the `Access-Control-Expose-Headers` header (which dioxus-fullstack does not
|
||||
//! currently expose directly).
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut headers = use_action(get_headers);
|
||||
|
||||
rsx! {
|
||||
h1 { "Header Map Example" }
|
||||
button { onclick: move |_| headers.call(), "Get Headers" }
|
||||
if let Some(Ok(headers)) = headers.result() {
|
||||
p { "Response from server:" }
|
||||
pre { "{headers}" }
|
||||
} else {
|
||||
p { "No headers yet" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/example", headers: dioxus::fullstack::HeaderMap)]
|
||||
async fn get_headers() -> Result<String> {
|
||||
Ok(format!("{:#?}", headers))
|
||||
}
|
||||
+4
-1
@@ -9,7 +9,10 @@ publish = false
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"]}
|
||||
serde = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -0,0 +1,3 @@
|
||||
h1 {
|
||||
font-family: monospace;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! A simple hello world example for Dioxus fullstack
|
||||
//!
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --web
|
||||
//! ```
|
||||
//!
|
||||
//! This example demonstrates a simple Dioxus fullstack application with a client-side counter
|
||||
//! and a server function that returns a greeting message.
|
||||
//!
|
||||
//! The `use_action` hook makes it easy to call async work (like server functions) from the client side
|
||||
//! and handle loading and error states.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::get;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut message = use_action(get_greeting);
|
||||
|
||||
rsx! {
|
||||
div { style: "padding: 2rem; font-family: Arial, sans-serif;",
|
||||
h1 { "Hello, Dioxus Fullstack!" }
|
||||
|
||||
// Client-side counter - you can use any client functionality in your app!
|
||||
div { style: "margin: 1rem 0;",
|
||||
h2 { "Client Counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Increment" }
|
||||
button { onclick: move |_| count -= 1, "Decrement" }
|
||||
}
|
||||
|
||||
// We can handle the action result and display loading state
|
||||
div { style: "margin: 1rem 0;",
|
||||
h2 { "Server Greeting" }
|
||||
button { onclick: move |_| message.call("World".to_string(), 30), "Get Server Greeting" }
|
||||
if message.is_pending() {
|
||||
p { "Loading..." }
|
||||
}
|
||||
p { "{message:#?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple server function that returns a greeting
|
||||
///
|
||||
/// Our server function takes a name as a path and query parameters as inputs and returns a greeting message.
|
||||
#[get("/api/greeting/{name}/{age}")]
|
||||
async fn get_greeting(name: String, age: i32) -> Result<String> {
|
||||
Ok(format!(
|
||||
"Hello from the server, {}! You are {} years old. 🚀",
|
||||
name, age
|
||||
))
|
||||
}
|
||||
Loaded 100 of 424 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user