Chore: hoist example projects for discoverability (#2959)

* hoist example projects, remove oidc

* move over example projects from the separate repo

* Update ecommerce site to the latest version of dioxus

* update wifi scanner to 0.5

* drop the chatbot example; might be restored in a separate PR

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
Jonathan Kelley 2024-09-16 07:11:33 -07:00 committed by GitHub
parent a9f87384f8
commit 983fcfc616
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 2774 additions and 7322 deletions

View file

@ -1,9 +1,3 @@
# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace.
[env]
DIOXUS_FRONT_ISSUER_URL = ""
DIOXUS_FRONT_CLIENT_ID = ""
DIOXUS_FRONT_URL = ""
[profile] [profile]
[profile.dioxus-client] [profile.dioxus-client]

231
Cargo.lock generated
View file

@ -1280,6 +1280,42 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "cached"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700"
dependencies = [
"async-trait",
"cached_proc_macro",
"cached_proc_macro_types",
"futures",
"hashbrown 0.13.2",
"instant",
"once_cell",
"thiserror",
"tokio",
]
[[package]]
name = "cached_proc_macro"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082"
dependencies = [
"cached_proc_macro_types",
"darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "cached_proc_macro_types"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@ -2266,14 +2302,38 @@ dependencies = [
"syn 2.0.74", "syn 2.0.74",
] ]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core 0.14.4",
"darling_macro 0.14.4",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.10" version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.10",
"darling_macro", "darling_macro 0.20.10",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
] ]
[[package]] [[package]]
@ -2290,13 +2350,24 @@ dependencies = [
"syn 2.0.74", "syn 2.0.74",
] ]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core 0.14.4",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.10" version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.10",
"quote", "quote",
"syn 2.0.74", "syn 2.0.74",
] ]
@ -2521,7 +2592,7 @@ dependencies = [
"ratatui", "ratatui",
"rayon", "rayon",
"regex", "regex",
"reqwest", "reqwest 0.12.5",
"rsx-rosetta", "rsx-rosetta",
"rustls 0.23.12", "rustls 0.23.12",
"serde", "serde",
@ -2587,7 +2658,7 @@ dependencies = [
"manganis", "manganis",
"pretty_assertions", "pretty_assertions",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest 0.12.5",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"rustversion", "rustversion",
"serde", "serde",
@ -2646,7 +2717,7 @@ dependencies = [
"objc", "objc",
"objc_id", "objc_id",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest 0.12.5",
"rfd", "rfd",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"separator", "separator",
@ -2679,7 +2750,7 @@ dependencies = [
"http-range", "http-range",
"manganis", "manganis",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest 0.12.5",
"separator", "separator",
"serde", "serde",
"serde_json", "serde_json",
@ -2754,7 +2825,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"generational-box", "generational-box",
"reqwest", "reqwest 0.12.5",
"rustversion", "rustversion",
"slab", "slab",
"tokio", "tokio",
@ -2887,6 +2958,16 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "dioxus-logger"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7cbab0b5519060fe9e14b3c21e3f2329b8386cd905618f78c7b929cd00cf54"
dependencies = [
"log",
"web-sys",
]
[[package]] [[package]]
name = "dioxus-mobile" name = "dioxus-mobile"
version = "0.6.0-alpha.2" version = "0.6.0-alpha.2"
@ -3014,7 +3095,7 @@ dependencies = [
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest 0.12.5",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"serde", "serde",
"simple_logger", "simple_logger",
@ -3240,6 +3321,17 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "ecommerce-site"
version = "0.1.1"
dependencies = [
"cached",
"chrono",
"dioxus",
"reqwest 0.11.27",
"serde",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -3306,7 +3398,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242"
dependencies = [ dependencies = [
"darling", "darling 0.20.10",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.74", "syn 2.0.74",
@ -3527,6 +3619,16 @@ dependencies = [
"rustc_version 0.4.0", "rustc_version 0.4.0",
] ]
[[package]]
name = "file-explorer"
version = "0.1.0"
dependencies = [
"dioxus",
"dioxus-logger",
"log",
"open",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.24" version = "0.2.24"
@ -3718,7 +3820,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"dioxus", "dioxus",
"reqwest", "reqwest 0.12.5",
"serde", "serde",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -3730,7 +3832,7 @@ name = "fullstack-hello-world-example"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dioxus", "dioxus",
"reqwest", "reqwest 0.12.5",
"serde", "serde",
"simple_logger", "simple_logger",
"tracing", "tracing",
@ -5178,6 +5280,19 @@ dependencies = [
"tokio-io-timeout", "tokio-io-timeout",
] ]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.30",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.6.0" version = "0.6.0"
@ -5565,6 +5680,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -6167,7 +6291,7 @@ dependencies = [
"railwind", "railwind",
"ravif", "ravif",
"rayon", "rayon",
"reqwest", "reqwest 0.12.5",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"serde", "serde",
"serde_json", "serde_json",
@ -6188,7 +6312,7 @@ dependencies = [
"built", "built",
"home", "home",
"infer 0.11.0", "infer 0.11.0",
"reqwest", "reqwest 0.12.5",
"serde", "serde",
"toml 0.7.8", "toml 0.7.8",
"tracing", "tracing",
@ -8143,6 +8267,46 @@ dependencies = [
"bytecheck", "bytecheck",
] ]
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper-tls 0.5.0",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.5" version = "0.12.5"
@ -8161,7 +8325,7 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper 1.4.1", "hyper 1.4.1",
"hyper-rustls", "hyper-rustls",
"hyper-tls", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -8829,7 +8993,7 @@ version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
dependencies = [ dependencies = [
"darling", "darling 0.20.10",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.74", "syn 2.0.74",
@ -8853,7 +9017,7 @@ dependencies = [
"inventory", "inventory",
"js-sys", "js-sys",
"once_cell", "once_cell",
"reqwest", "reqwest 0.12.5",
"send_wrapper", "send_wrapper",
"serde", "serde",
"serde_json", "serde_json",
@ -12207,6 +12371,29 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "wifi-scanner"
version = "0.1.1"
dependencies = [
"anyhow",
"dioxus",
"futures",
"futures-channel",
"pretty_env_logger",
"tokio",
"wifiscanner",
]
[[package]]
name = "wifiscanner"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d74e667d34abc74f1e5b509ae3d24641f56514ba2c5a64f9d733ae2061aef856"
dependencies = [
"itertools 0.8.2",
"regex",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -12605,6 +12792,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.51.0" version = "0.51.0"

View file

@ -32,22 +32,24 @@ members = [
"packages/static-generation", "packages/static-generation",
"packages/lazy-js-bundle", "packages/lazy-js-bundle",
# Fullstack examples
"packages/fullstack/examples/hello-world",
"packages/fullstack/examples/router",
"packages/fullstack/examples/streaming",
"packages/fullstack/examples/desktop",
"packages/fullstack/examples/auth",
"packages/fullstack/examples/hackernews",
# Static generation examples
"packages/static-generation/examples/simple",
"packages/static-generation/examples/router",
"packages/static-generation/examples/github-pages",
# Full project examples # Full project examples
"example-projects/fullstack-hackernews",
"example-projects/ecommerce-site",
"example-projects/wifi-scanner",
"example-projects/file-explorer",
# Simple examples that require a crate
"examples/tailwind", "examples/tailwind",
"examples/PWA-example", "examples/pwa",
# "examples/openid_connect_demo", "examples/fullstack-hello-world",
"examples/fullstack-router",
"examples/fullstack-streaming",
"examples/fullstack-desktop",
"examples/fullstack-auth",
"examples/ssg-simple",
"examples/ssg-router",
"examples/ssg-github-pages",
# Playwright tests # Playwright tests
"packages/playwright-tests/liveview", "packages/playwright-tests/liveview",
"packages/playwright-tests/web", "packages/playwright-tests/web",
@ -310,11 +312,6 @@ name = "errors"
required-features = ["desktop"] required-features = ["desktop"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "file_explorer"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]] [[example]]
name = "future" name = "future"
doc-scrape-examples = true doc-scrape-examples = true

View file

@ -0,0 +1 @@
/target

View file

@ -0,0 +1,17 @@
[package]
name = "ecommerce-site"
version = "0.1.1"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cached = "0.44.0"
dioxus = { workspace = true, features = ["fullstack", "router"] }
reqwest = { version = "0.11.16", features = ["json"] }
serde = "1.0.160"
chrono = { version = "0.4.24", features = ["serde"] }
[features]
web = ["dioxus/web"]
server = ["dioxus/server"]

View file

@ -0,0 +1,24 @@
# Dioxus Example: An e-commerce site using the FakeStoreAPI
This example app is a fullstack web application leveraging the [FakeStoreAPI](https://fakestoreapi.com) and [Tailwind CSS](https://tailwindcss.com/).
![Demo Image](demo.png)
# Development
1. Run the following commands to serve the application (see the tailwind example in the main Dioxus repo for more detailed information about setting up tailwind):
```bash
npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch
dx serve
```
# Status
This is a work in progress. The following features are currently implemented:
- [x] A homepage with a list of products dynamically fetched from the FakeStoreAPI (rendered using SSR)
- [x] A product detail page with details about a product (rendered using LiveView)
- [ ] A cart page
- [ ] A checkout page
- [ ] A login page

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,160 @@
#![allow(unused)]
use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Product {
pub(crate) id: u32,
pub(crate) title: String,
pub(crate) price: f32,
pub(crate) description: String,
pub(crate) category: String,
pub(crate) image: String,
pub(crate) rating: Rating,
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Rating {
pub(crate) rate: f32,
pub(crate) count: u32,
}
impl Display for Rating {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let rounded = self.rate.round() as usize;
for _ in 0..rounded {
"".fmt(f)?;
}
for _ in 0..(5 - rounded) {
"".fmt(f)?;
}
write!(f, " ({:01}) ({} ratings)", self.rate, self.count)?;
Ok(())
}
}
#[allow(unused)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd)]
pub(crate) enum Sort {
Descending,
Ascending,
}
impl Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Sort::Descending => write!(f, "desc"),
Sort::Ascending => write!(f, "asc"),
}
}
}
// Cache up to 100 requests, invalidating them after 60 seconds
#[cached::proc_macro::cached(size = 100, time = 60, result = true)]
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
#[cached::proc_macro::cached(size = 100, time = 60, result = true)]
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
#[cached::proc_macro::cached(size = 100, time = 60, result = true)]
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
#[cached::proc_macro::cached(size = 100, time = 60, result = true)]
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 {
id: usize,
email: String,
username: String,
password: String,
name: FullName,
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,
lastname: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct Cart {
id: usize,
#[serde(rename = "userId")]
user_id: usize,
data: String,
products: Vec<ProductInCart>,
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 {
#[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
}
}

View file

@ -0,0 +1,14 @@
use dioxus::prelude::*;
#[component]
pub fn error_page() -> Element {
rsx! {
section { class: "py-20",
div { class: "container mx-auto px-4",
div { class: "flex flex-wrap -mx-4 mb-24 text-center",
"An internal error has occurred"
}
}
}
}
}

View file

@ -0,0 +1,24 @@
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
use crate::{
api::{fetch_products, Sort},
components::nav,
components::product_item::product_item,
};
use dioxus::prelude::*;
pub(crate) fn Home() -> Element {
let products = use_server_future(|| fetch_products(10, Sort::Ascending))?;
let products = products().unwrap()?;
rsx! {
nav::nav {}
section { class: "p-10",
for product in products {
product_item {
product
}
}
}
}
}

View file

@ -0,0 +1,32 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
head::Link {
rel: "stylesheet",
href: asset!("./public/loading.css")
}
SuspenseBoundary {
fallback: |context: SuspenseContext| {
rsx! {
if let Some(placeholder) = context.suspense_placeholder() {
{placeholder}
} else {
LoadingIndicator {}
}
}
},
{children}
}
}
}
#[component]
fn LoadingIndicator() -> Element {
rsx! {
div {
class: "spinner",
}
}
}

View file

@ -0,0 +1,389 @@
use dioxus::prelude::*;
pub fn nav() -> Element {
rsx! {
section { class: "relative",
nav { class: "flex justify-between border-b",
div { class: "px-12 py-8 flex w-full items-center",
a { class: "hidden xl:block mr-16",
href: "/",
icons::cart_icon {}
}
ul { class: "hidden xl:flex font-semibold font-heading",
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Category"
}
}
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Collection"
}
}
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Story"
}
}
li {
a { class: "hover:text-gray-600",
href: "/",
"Brand"
}
}
}
a { class: "flex-shrink-0 xl:mx-auto text-3xl font-bold font-heading",
href: "/",
img { class: "h-9",
width: "auto",
alt: "",
src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg",
}
}
div { class: "hidden xl:inline-block mr-14",
input { class: "py-5 px-8 w-full placeholder-gray-400 text-xs uppercase font-semibold font-heading bg-gray-50 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
placeholder: "Search",
r#type: "text",
}
}
div { class: "hidden xl:flex items-center",
a { class: "mr-10 hover:text-gray-600",
href: "",
icons::icon_1 {}
}
a { class: "flex items-center hover:text-gray-600",
href: "/",
icons::icon_2 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading",
"3"
}
}
}
}
a { class: "hidden xl:flex items-center px-12 border-l font-semibold font-heading hover:text-gray-600",
href: "/",
icons::icon_3 {}
span {
"Sign In"
}
}
a { class: "xl:hidden flex mr-6 items-center text-gray-600",
href: "/",
icons::icon_4 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading",
"3"
}
}
a { class: "navbar-burger self-center mr-12 xl:hidden",
href: "/",
icons::icon_5 {}
}
}
div { class: "hidden navbar-menu fixed top-0 left-0 bottom-0 w-5/6 max-w-sm z-50",
div { class: "navbar-backdrop fixed inset-0 bg-gray-800 opacity-25",
}
nav { class: "relative flex flex-col py-6 px-6 w-full h-full bg-white border-r overflow-y-auto",
div { class: "flex items-center mb-8",
a { class: "mr-auto text-3xl font-bold font-heading",
href: "/",
img { class: "h-9",
src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg",
width: "auto",
alt: "",
}
}
button { class: "navbar-close",
icons::icon_6 {}
}
}
div { class: "flex mb-8 justify-between",
a { class: "inline-flex items-center font-semibold font-heading",
href: "/",
icons::icon_7 {}
span {
"Sign In"
}
}
div { class: "flex items-center",
a { class: "mr-10",
href: "/",
icons::icon_8 {}
}
a { class: "flex items-center",
href: "/",
icons::icon_9 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-100 rounded-full font-semibold font-heading",
"3"
}
}
}
}
input { class: "block mb-10 py-5 px-8 bg-gray-100 rounded-md border-transparent focus:ring-blue-300 focus:border-blue-300 focus:outline-none",
r#type: "search",
placeholder: "Search",
}
ul { class: "text-3xl font-bold font-heading",
li { class: "mb-8",
a {
href: "/",
"Category"
}
}
li { class: "mb-8",
a {
href: "/",
"Collection"
}
}
li { class: "mb-8",
a {
href: "/",
"Story"
}
}
li {
a {
href: "/",
"Brand"
}
}
}
}
}
}
}
}
mod icons {
use super::*;
pub(super) fn cart_icon() -> Element {
rsx! {
svg { class: "mr-3",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
view_box: "0 0 23 23",
width: "23",
height: "23",
path {
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
stroke: "currentColor",
stroke_linecap: "round",
stroke_width: "1.5",
}
path {
stroke: "currentColor",
stroke_linecap: "round",
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_width: "1.5",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_1() -> Element {
rsx! {
svg {
xmlns: "http://www.w3.org/2000/svg",
height: "20",
view_box: "0 0 23 20",
width: "23",
fill: "none",
path {
d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linejoin: "round",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_2() -> Element {
rsx! {
svg { class: "mr-3",
fill: "none",
height: "31",
xmlns: "http://www.w3.org/2000/svg",
width: "32",
view_box: "0 0 32 31",
path {
stroke_linejoin: "round",
stroke_width: "1.5",
d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z",
stroke_linecap: "round",
stroke: "currentColor",
}
path {
stroke_width: "1.5",
d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963",
stroke: "currentColor",
stroke_linecap: "round",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_3() -> Element {
rsx! {
svg { class: "h-2 w-2 text-gray-500 cursor-pointer",
height: "10",
width: "10",
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
view_box: "0 0 10 10",
path {
stroke_width: "1.5",
stroke_linejoin: "round",
d: "M9.00002 1L1 9.00002M1.00003 1L9.00005 9.00002",
stroke: "black",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_4() -> Element {
rsx! {
svg {
view_box: "0 0 20 12",
fill: "none",
width: "20",
xmlns: "http://www.w3.org/2000/svg",
height: "12",
path {
d: "M1 2H19C19.2652 2 19.5196 1.89464 19.7071 1.70711C19.8946 1.51957 20 1.26522 20 1C20 0.734784 19.8946 0.48043 19.7071 0.292893C19.5196 0.105357 19.2652 0 19 0H1C0.734784 0 0.48043 0.105357 0.292893 0.292893C0.105357 0.48043 0 0.734784 0 1C0 1.26522 0.105357 1.51957 0.292893 1.70711C0.48043 1.89464 0.734784 2 1 2ZM19 10H1C0.734784 10 0.48043 10.1054 0.292893 10.2929C0.105357 10.4804 0 10.7348 0 11C0 11.2652 0.105357 11.5196 0.292893 11.7071C0.48043 11.8946 0.734784 12 1 12H19C19.2652 12 19.5196 11.8946 19.7071 11.7071C19.8946 11.5196 20 11.2652 20 11C20 10.7348 19.8946 10.4804 19.7071 10.2929C19.5196 10.1054 19.2652 10 19 10ZM19 5H1C0.734784 5 0.48043 5.10536 0.292893 5.29289C0.105357 5.48043 0 5.73478 0 6C0 6.26522 0.105357 6.51957 0.292893 6.70711C0.48043 6.89464 0.734784 7 1 7H19C19.2652 7 19.5196 6.89464 19.7071 6.70711C19.8946 6.51957 20 6.26522 20 6C20 5.73478 19.8946 5.48043 19.7071 5.29289C19.5196 5.10536 19.2652 5 19 5Z",
fill: "#8594A5",
}
}
}
}
pub(super) fn icon_5() -> Element {
rsx! {
svg { class: "mr-2",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
width: "23",
height: "23",
view_box: "0 0 23 23",
path {
stroke_width: "1.5",
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
stroke: "currentColor",
}
path {
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_linejoin: "round",
stroke_width: "1.5",
stroke_linecap: "round",
stroke: "currentColor",
}
}
}
}
pub(super) fn icon_6() -> Element {
rsx! {
svg { class: "mr-3",
height: "31",
xmlns: "http://www.w3.org/2000/svg",
view_box: "0 0 32 31",
width: "32",
fill: "none",
path {
stroke: "currentColor",
stroke_width: "1.5",
d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z",
stroke_linecap: "round",
stroke_linejoin: "round",
}
path {
stroke_linecap: "round",
stroke_width: "1.5",
stroke: "currentColor",
stroke_linejoin: "round",
d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963",
}
}
}
}
pub(super) fn icon_7() -> Element {
rsx! {
svg { class: "mr-3",
view_box: "0 0 23 23",
fill: "none",
height: "23",
width: "23",
xmlns: "http://www.w3.org/2000/svg",
path {
stroke_linecap: "round",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
}
path {
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_width: "1.5",
stroke_linecap: "round",
stroke: "currentColor",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_8() -> Element {
rsx! {
svg {
height: "20",
width: "23",
fill: "none",
view_box: "0 0 23 20",
xmlns: "http://www.w3.org/2000/svg",
path {
d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z",
stroke_linejoin: "round",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_9() -> Element {
rsx! {
svg {
view_box: "0 0 18 18",
xmlns: "http://www.w3.org/2000/svg",
width: "18",
height: "18",
fill: "none",
path {
fill: "black",
d: "M18 15.4688H0V17.7207H18V15.4688Z",
}
path {
fill: "black",
d: "M11.0226 7.87402H0V10.126H11.0226V7.87402Z",
}
path {
fill: "black",
d: "M18 0.279297H0V2.53127H18V0.279297Z",
}
}
}
}
}

View file

@ -0,0 +1,44 @@
use dioxus::prelude::*;
use crate::api::Product;
#[component]
pub(crate) fn product_item(product: Product) -> Element {
let Product {
id,
title,
price,
category,
image,
rating,
..
} = product;
rsx! {
section { class: "h-40 p-2 m-2 shadow-lg ring-1 rounded-lg flex flex-row place-items-center hover:ring-4 hover:shadow-2xl transition-all duration-200",
img {
class: "object-scale-down w-1/6 h-full",
src: "{image}",
}
div { class: "pl-4 text-left text-ellipsis",
a {
href: "/details/{id}",
class: "w-full text-center",
"{title}"
}
p {
class: "w-full",
"{rating}"
}
p {
class: "w-full",
"{category}"
}
p {
class: "w-1/4",
"${price}"
}
}
}
}
}

View file

@ -0,0 +1,309 @@
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: ReadOnlySignal<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 {
title,
price,
description,
category,
image,
rating,
..
} = product().unwrap()?;
rsx! {
section { class: "py-20",
div { class: "container mx-auto px-4",
div { class: "flex flex-wrap -mx-4 mb-24",
div { class: "w-full md:w-1/2 px-4 mb-8 md:mb-0",
div { class: "relative mb-10",
style: "height: 564px;",
a { class: "absolute top-1/2 left-0 ml-8 transform translate-1/2",
href: "#",
icons::icon_0 {}
}
img { class: "object-cover w-full h-full",
alt: "",
src: "{image}",
}
a { class: "absolute top-1/2 right-0 mr-8 transform translate-1/2",
href: "#",
icons::icon_1 {}
}
}
}
div { class: "w-full md:w-1/2 px-4",
div { class: "lg:pl-20",
div { class: "mb-10 pb-10 border-b",
h2 { class: "mt-2 mb-6 max-w-xl text-5xl md:text-6xl font-bold font-heading",
"{title}"
}
div { class: "mb-8",
"{rating}"
}
p { class: "inline-block mb-8 text-2xl font-bold font-heading text-blue-300",
span {
"${price}"
}
}
p { class: "max-w-md text-gray-500",
"{description}"
}
}
div { class: "flex mb-12",
div { class: "mr-6",
span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase",
"QTY"
}
div { class: "inline-flex items-center px-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
button { class: "py-2 hover:text-gray-700",
onclick: move |_| quantity += 1,
icons::icon_2 {}
}
input { class: "w-12 m-0 px-2 py-4 text-center md:text-right border-0 focus:ring-transparent focus:outline-none rounded-md",
placeholder: "1",
r#type: "number",
value: "{quantity}",
oninput: move |evt| if let Ok(as_number) = evt.value().parse() { quantity.set(as_number) },
}
button { class: "py-2 hover:text-gray-700",
onclick: move |_| quantity -= 1,
icons::icon_3 {}
}
}
}
div {
span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase",
"Size"
}
select { class: "pl-6 pr-10 py-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
id: "",
name: "",
onchange: move |evt| {
if let Ok(new_size) = evt.value().parse() {
size.set(new_size);
}
},
option {
value: "1",
"Medium"
}
option {
value: "2",
"Small"
}
option {
value: "3",
"Large"
}
}
}
}
div { class: "flex flex-wrap -mx-4 mb-14 items-center",
div { class: "w-full xl:w-2/3 px-4 mb-4 xl:mb-0",
a { class: "block bg-orange-300 hover:bg-orange-400 text-center text-white font-bold font-heading py-5 px-8 rounded-md uppercase transition duration-200",
href: "#",
"Add to cart"
}
}
}
div { class: "flex items-center",
span { class: "mr-8 text-gray-500 font-bold font-heading uppercase",
"SHARE IT"
}
a { class: "mr-1 w-8 h-8",
href: "#",
img {
alt: "",
src: "https://shuffle.dev/yofte-assets/buttons/facebook-circle.svg",
}
}
a { class: "mr-1 w-8 h-8",
href: "#",
img {
alt: "",
src: "https://shuffle.dev/yofte-assets/buttons/instagram-circle.svg",
}
}
a { class: "w-8 h-8",
href: "#",
img {
src: "https://shuffle.dev/yofte-assets/buttons/twitter-circle.svg",
alt: "",
}
}
}
}
}
}
div {
ul { class: "flex flex-wrap mb-16 border-b-2",
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 bg-white text-gray-500 font-bold font-heading shadow-2xl",
href: "#",
"Description"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Customer reviews"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Shipping &amp; returns"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Brand"
}
}
}
h3 { class: "mb-8 text-3xl font-bold font-heading text-blue-300",
"{category}"
}
p { class: "max-w-2xl text-gray-500",
"{description}"
}
}
}
}
}
}
mod icons {
use super::*;
pub(super) fn icon_0() -> Element {
rsx! {
svg { class: "w-6 h-6",
view_box: "0 0 24 23",
xmlns: "http://www.w3.org/2000/svg",
height: "23",
fill: "none",
width: "24",
path {
stroke: "black",
fill: "black",
d: "M2.01328 18.9877C2.05682 16.7902 2.71436 12.9275 6.3326 9.87096L6.33277 9.87116L6.33979 9.86454L6.3398 9.86452C6.34682 9.85809 8.64847 7.74859 13.4997 7.74859C13.6702 7.74859 13.8443 7.75111 14.0206 7.757L14.0213 7.75702L14.453 7.76978L14.6331 7.77511V7.59486V3.49068L21.5728 10.5736L14.6331 17.6562V13.6558V13.5186L14.4998 13.4859L14.1812 13.4077C14.1807 13.4075 14.1801 13.4074 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M2.01328 18.9877C7.16281 11.8391 14.012 13.3662 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M23.125 10.6961L23.245 10.5736L23.125 10.4512L13.7449 0.877527L13.4449 0.571334V1V6.5473C8.22585 6.54663 5.70981 8.81683 5.54923 8.96832C-0.317573 13.927 0.931279 20.8573 0.946581 20.938L0.946636 20.9383L1.15618 22.0329L1.24364 22.4898L1.47901 22.0885L2.041 21.1305L2.04103 21.1305C4.18034 17.4815 6.71668 15.7763 8.8873 15.0074C10.9246 14.2858 12.6517 14.385 13.4449 14.4935V20.1473V20.576L13.7449 20.2698L23.125 10.6961Z",
stroke_width: "0.35",
}
}
}
}
pub(super) fn icon_1() -> Element {
rsx! {
svg { class: "w-6 h-6",
height: "27",
view_box: "0 0 27 27",
fill: "none",
width: "27",
xmlns: "http://www.w3.org/2000/svg",
path {
d: "M13.4993 26.2061L4.70067 16.9253C3.9281 16.1443 3.41815 15.1374 3.24307 14.0471C3.06798 12.9568 3.23664 11.8385 3.72514 10.8505V10.8505C4.09415 10.1046 4.63318 9.45803 5.29779 8.96406C5.96241 8.47008 6.73359 8.14284 7.54782 8.00931C8.36204 7.87578 9.19599 7.93978 9.98095 8.19603C10.7659 8.45228 11.4794 8.89345 12.0627 9.48319L13.4993 10.9358L14.9359 9.48319C15.5192 8.89345 16.2327 8.45228 17.0177 8.19603C17.8026 7.93978 18.6366 7.87578 19.4508 8.00931C20.265 8.14284 21.0362 8.47008 21.7008 8.96406C22.3654 9.45803 22.9045 10.1046 23.2735 10.8505V10.8505C23.762 11.8385 23.9306 12.9568 23.7556 14.0471C23.5805 15.1374 23.0705 16.1443 22.298 16.9253L13.4993 26.2061Z",
stroke: "black",
stroke_width: "1.5",
stroke_linecap: "round",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_2() -> Element {
rsx! {
svg {
view_box: "0 0 12 12",
height: "12",
width: "12",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
g {
opacity: "0.35",
rect {
height: "12",
x: "5",
fill: "currentColor",
width: "2",
}
rect {
fill: "currentColor",
width: "2",
height: "12",
x: "12",
y: "5",
transform: "rotate(90 12 5)",
}
}
}
}
}
pub(super) fn icon_3() -> Element {
rsx! {
svg {
width: "12",
fill: "none",
view_box: "0 0 12 2",
height: "2",
xmlns: "http://www.w3.org/2000/svg",
g {
opacity: "0.35",
rect {
transform: "rotate(90 12 0)",
height: "12",
fill: "currentColor",
x: "12",
width: "2",
}
}
}
}
}
}

View file

@ -0,0 +1,52 @@
#![allow(non_snake_case)]
use components::home::Home;
use components::loading::ChildrenOrLoading;
use dioxus::prelude::*;
mod components {
pub mod error;
pub mod home;
pub mod loading;
pub mod nav;
pub mod product_item;
pub mod product_page;
}
mod api;
fn main() {
launch(|| {
rsx! {
head::Link {
rel: "stylesheet",
href: asset!("./public/tailwind.css")
}
ChildrenOrLoading {
Router::<Route> {}
}
}
});
}
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/details/:product_id")]
Details { product_id: usize },
}
#[component]
/// Render a more sophisticated page with ssr
fn Details(product_id: usize) -> Element {
rsx! {
div {
components::nav::nav {}
components::product_page::product_page {
product_id
}
}
}
}

View file

@ -0,0 +1,13 @@
module.exports = {
mode: "all",
content: [
// include all rust, html and css files in the src directory
"./src/**/*.{rs,html,css}",
// include all html files in the output (dist) directory
"./dist/**/*.html",
],
theme: {
extend: {},
},
plugins: [],
};

View file

@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/dist/
/static/
/.dioxus/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View file

@ -0,0 +1,14 @@
[package]
name = "file-explorer"
edition = "2021"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true, features = ["desktop"] }
# Debug
log = "0.4.19"
dioxus-logger = "0.4.1"
open = "5.1.2"

View file

@ -0,0 +1,43 @@
[application]
# App (Project) Name
name = "file-explorer"
# Dioxus App Default Platform
# desktop, web
default_platform = "desktop"
# `build` & `serve` dist path
out_dir = "dist"
# assets file folder
asset_dir = "assets"
[web.app]
# HTML title tag content
title = "file-explorer"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View file

@ -0,0 +1,13 @@
# File-explorer with Rust and Dioxus
This example shows how a Dioxus App can directly leverage system calls and libraries to bridge native functionality with the WebView renderer.
![example](./src/image.png)
## To run this example:
```
dx serve
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -5,6 +5,9 @@
//! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do. //! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do.
//! We store the state entirely in a single signal, making the explorer logic fairly easy to reason about. //! We store the state entirely in a single signal, making the explorer logic fairly easy to reason about.
use std::env::current_dir;
use std::path::PathBuf;
use dioxus::desktop::{Config, WindowBuilder}; use dioxus::desktop::{Config, WindowBuilder};
use dioxus::prelude::*; use dioxus::prelude::*;
@ -20,7 +23,7 @@ fn app() -> Element {
rsx! { rsx! {
head::Link { head::Link {
rel: "stylesheet", rel: "stylesheet",
href: asset!("./examples/assets/fileexplorer.css") href: asset!("./assets/fileexplorer.css")
} }
div { div {
head::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" } head::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
@ -33,7 +36,8 @@ fn app() -> Element {
main { main {
for (dir_id, path) in files.read().path_names.iter().enumerate() { for (dir_id, path) in files.read().path_names.iter().enumerate() {
{ {
let path_end = path.split('/').last().unwrap_or(path.as_str()); let path_end = path.components().last().map(|p|p.as_os_str()).unwrap_or(path.as_os_str()).to_string_lossy();
let path = path.display();
rsx! { rsx! {
div { class: "folder", key: "{path}", div { class: "folder", key: "{path}",
i { class: "material-icons", i { class: "material-icons",
@ -66,15 +70,15 @@ fn app() -> Element {
/// We don't use any fancy signals or memoization here - Dioxus is so fast that even a file explorer can be done with a /// We don't use any fancy signals or memoization here - Dioxus is so fast that even a file explorer can be done with a
/// single signal. /// single signal.
struct Files { struct Files {
path_stack: Vec<String>, current_path: PathBuf,
path_names: Vec<String>, path_names: Vec<PathBuf>,
err: Option<String>, err: Option<String>,
} }
impl Files { impl Files {
fn new() -> Self { fn new() -> Self {
let mut files = Self { let mut files = Self {
path_stack: vec!["./".to_string()], current_path: std::path::absolute(current_dir().unwrap()).unwrap(),
path_names: vec![], path_names: vec![],
err: None, err: None,
}; };
@ -85,13 +89,11 @@ impl Files {
} }
fn reload_path_list(&mut self) { fn reload_path_list(&mut self) {
let cur_path = self.path_stack.last().unwrap(); let paths = match std::fs::read_dir(&self.current_path) {
let paths = match std::fs::read_dir(cur_path) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
let err = format!("An error occurred: {err:?}"); let err = format!("An error occurred: {err:?}");
self.err = Some(err); self.err = Some(err);
self.path_stack.pop();
return; return;
} }
}; };
@ -102,27 +104,31 @@ impl Files {
self.path_names.clear(); self.path_names.clear();
for path in collected { for path in collected {
self.path_names self.path_names.push(path.unwrap().path().to_path_buf());
.push(path.unwrap().path().display().to_string());
} }
} }
fn go_up(&mut self) { fn go_up(&mut self) {
if self.path_stack.len() > 1 { self.current_path = match self.current_path.parent() {
self.path_stack.pop(); Some(path) => path.to_path_buf(),
} None => {
self.err = Some("Cannot go up from the root directory".to_string());
return;
}
};
self.reload_path_list(); self.reload_path_list();
} }
fn enter_dir(&mut self, dir_id: usize) { fn enter_dir(&mut self, dir_id: usize) {
let path = &self.path_names[dir_id]; let path = &self.path_names[dir_id];
self.path_stack.push(path.clone()); self.current_path = path.clone();
self.reload_path_list(); self.reload_path_list();
} }
fn current(&self) -> &str { fn current(&self) -> String {
self.path_stack.last().unwrap() self.current_path.display().to_string()
} }
fn clear_err(&mut self) { fn clear_err(&mut self) {
self.err = None; self.err = None;
} }

View file

@ -0,0 +1,17 @@
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
width: 10px;
height: 10px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}

View file

@ -0,0 +1 @@
/target

View file

@ -0,0 +1,15 @@
[package]
name = "wifi-scanner"
version = "0.1.1"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.52"
pretty_env_logger = "0.5.0"
tokio = { version = "1.12.0", features = ["full"] }
dioxus = { workspace = true, features = ["desktop"] }
wifiscanner = "0.5.1"
futures-channel = "0.3.19"
futures = "0.3.19"

View file

@ -0,0 +1,5 @@
# WiFi scanner app
This desktop app showcases the use of background threads.
![Demo of app](./demo_small.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1,91 @@
use dioxus::prelude::*;
use wifiscanner::Wifi;
fn main() {
launch(app)
}
fn perform_scan() -> Status {
if let Ok(devices) = wifiscanner::scan() {
if devices.is_empty() {
Status::NoneFound
} else {
Status::Found(devices)
}
} else {
Status::NoneFound
}
}
enum Status {
NoneFound,
Found(Vec<Wifi>),
}
fn app() -> Element {
let mut status =
use_resource(|| async { tokio::task::spawn_blocking(perform_scan).await.unwrap() });
let scanning = !status.finished();
rsx! {
link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" },
div {
div { class: "py-8 px-6",
div { class: "container px-4 mx-auto",
h2 { class: "text-2xl font-bold", "Scan for WiFi Networks" }
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: scanning,
onclick: move |_| {
status.restart();
},
if scanning { "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 &*status.read() {
None => rsx!(""),
Some(Status::NoneFound) => rsx!("No networks found. Try scanning again"),
Some(Status::Found(wifis)) => {
// Create vector of tuples of (signal_level, wifi) for sorting by signal_level
let mut sorted_wifis = wifis
.iter()
.map(|wif: &Wifi| (wif, wif.signal_level.parse::<f32>().unwrap()))
.collect::<Vec<_>>();
sorted_wifis.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
rsx! {
tbody {
for (Wifi { mac: _, ssid, channel, signal_level, security }, _) in sorted_wifis.into_iter().rev() {
tr { class: "text-xs bg-gray-50",
td { class: "py-5 px-6 font-medium", "{signal_level}" }
td { class: "flex py-3 font-medium", "{ssid}" }
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{channel}" } }
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{security}" } }
}
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -1,5 +0,0 @@
[env]
DIOXUS_FRONT_ISSUER_URL = "TODO"
DIOXUS_FRONT_CLIENT_ID = "TODO"
DIOXUS_FRONT_CLIENT_SECRET = "TODO"
DIOXUS_FRONT_URL = "http://localhost:8080"

View file

@ -1,3 +0,0 @@
/target
/dist
.env

File diff suppressed because it is too large Load diff

View file

@ -1,36 +0,0 @@
[package]
name = "openid_auth_demo"
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]
anyhow = "1.0.86"
console_error_panic_hook = "0.1"
dioxus = { path = "../../packages/dioxus", default_features = true, features = [
"router",
"signals",
], version = "*" }
dioxus-logger = "0.5.1"
dioxus-sdk = { git = "https://github.com/Dioxuslabs/sdk", features = [
"storage",
] }
form_urlencoded = "1.2.1"
log = "0.4"
openidconnect = "3.5.0"
serde = { version = "1.0.203", features = ["derive"] }
uuid = "1.8"
[features]
default = ["web"]
server = ["dioxus/axum"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
fullstack = ["dioxus/fullstack"]
# since we're using dioxus from local path, inform dioxus-sdk to use it as well
[patch.crates-io]
dioxus = { path = "../../packages/dioxus" }
dioxus-signals = { path = "../../packages/signals" }

View file

@ -1,35 +0,0 @@
[application]
# dioxus project name
name = "OpenID Connect authentication demo"
# default platform
# you can also use `dioxus serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "web"
# Web `build` & `serve` dist path
out_dir = "dist"
# resource (static) file folder
asset_dir = "public"
# hot reload by default
hot_reload = true
[web.app]
# HTML title tag content
title = "OpenID Connect authentication demo"
[web.watcher]
index_on_404 = true
watch_path = ["src"]
[application.plugins]
available = true
required = []

View file

@ -1,12 +0,0 @@
# OpenID Connect example to show how to authenticate an user
The environment variables in [`.cargo/config.toml`](./.cargo/config.toml) must be set in order for this example to work.
Once they are set, you can run `dx serve --platform web` or `dx serve --platform desktop`.
### Environment variables summary
- `DIOXUS_FRONT_ISSUER_URL`: The openid-connect's issuer url
- `DIOXUS_FRONT_CLIENT_ID`: The openid-connect's client id
- `DIOXUS_FRONT_CLIENT_SECRET`: The openid-connect's client secret
- `DIOXUS_FRONT_URL`: The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`

View file

@ -1,2 +0,0 @@
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";

View file

@ -1,36 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_logger::tracing::Level;
use router::Route;
use crate::oidc::ClientState;
use crate::storage::{use_auth_request_provider, use_auth_token_provider};
pub(crate) mod constants;
pub(crate) mod model;
pub(crate) mod oidc;
pub(crate) mod props;
pub(crate) mod router;
pub(crate) mod storage;
pub(crate) mod views;
pub static CLIENT: GlobalSignal<ClientState> = Signal::global(ClientState::default);
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
pub static DIOXUS_FRONT_CLIENT_SECRET: &str = env!("DIOXUS_FRONT_CLIENT_SECRET");
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
fn App() -> Element {
use_auth_request_provider();
use_auth_token_provider();
rsx! { Router::<Route> {} }
}
fn main() {
dioxus_logger::init(Level::DEBUG).expect("failed to init logger");
dioxus_sdk::set_dir!();
console_error_panic_hook::set_once();
log::info!("starting app");
launch(App);
}

View file

@ -1 +0,0 @@
pub(crate) mod user;

View file

@ -1,7 +0,0 @@
use uuid::Uuid;
#[derive(PartialEq)]
pub struct User {
pub id: Uuid,
pub name: String,
}

View file

@ -1,127 +0,0 @@
use anyhow::Result;
use openidconnect::{
core::{CoreClient, CoreIdToken, CoreResponseType, CoreTokenResponse},
reqwest::async_http_client,
url::Url,
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, ClientSecret,
CsrfToken, IssuerUrl, LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl,
RefreshToken,
};
use serde::{Deserialize, Serialize};
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
#[derive(Clone, Debug, Default)]
pub struct ClientState {
pub oidc_client: Option<ClientProps>,
}
/// State that holds the nonce and authorization url and the nonce generated to log in an user
#[derive(Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct AuthRequestState {
pub auth_request: Option<AuthRequest>,
}
#[derive(Clone, PartialEq, Deserialize, Serialize)]
pub struct AuthRequest {
pub nonce: Nonce,
pub authorize_url: String,
}
/// State the tokens returned once the user is authenticated
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AuthTokenState {
/// Token used to identify the user
pub id_token: Option<CoreIdToken>,
/// Token used to refresh the tokens if they expire
pub refresh_token: Option<RefreshToken>,
}
impl PartialEq for AuthTokenState {
fn eq(&self, other: &Self) -> bool {
self.id_token == other.id_token
&& self.refresh_token.as_ref().map(|t| t.secret().clone())
== other.refresh_token.as_ref().map(|t| t.secret().clone())
}
}
pub fn email(
client: CoreClient,
id_token: CoreIdToken,
nonce: Nonce,
) -> Result<String, ClaimsVerificationError> {
match id_token.claims(&client.id_token_verifier(), &nonce) {
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
Err(error) => Err(error),
}
}
pub fn authorize_url(client: CoreClient) -> AuthRequest {
let (authorize_url, _csrf_state, nonce) = client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scope(openidconnect::Scope::new("email".to_string()))
.add_scope(openidconnect::Scope::new("profile".to_string()))
.url();
AuthRequest {
authorize_url: authorize_url.to_string(),
nonce,
}
}
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout> {
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
}
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient)> {
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
let provider_metadata = init_provider_metadata().await?;
let client_secret = Some(ClientSecret::new(
crate::DIOXUS_FRONT_CLIENT_SECRET.to_string(),
));
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
Ok((
client_id.clone(),
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
.set_redirect_uri(redirect_url),
))
}
///TODO: Add pkce_pacifier
pub async fn token_response(oidc_client: CoreClient, code: String) -> Result<CoreTokenResponse> {
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
Ok(oidc_client
.exchange_code(AuthorizationCode::new(code.clone()))
// .set_pkce_verifier(pkce_verifier)
.request_async(async_http_client)
.await?)
}
pub async fn exchange_refresh_token(
oidc_client: CoreClient,
refresh_token: RefreshToken,
) -> Result<CoreTokenResponse> {
Ok(oidc_client
.exchange_refresh_token(&refresh_token)
.request_async(async_http_client)
.await?)
}
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url> {
let provider_metadata = init_provider_metadata().await?;
let end_session_url = provider_metadata
.additional_metadata()
.clone()
.end_session_endpoint
.unwrap();
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
Ok(logout_request
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
.set_id_token_hint(&id_token_hint)
.http_get_url())
}

View file

@ -1,20 +0,0 @@
use dioxus::prelude::*;
use openidconnect::{core::CoreClient, ClientId};
#[derive(Props, Clone, Debug)]
pub struct ClientProps {
pub client: CoreClient,
pub client_id: ClientId,
}
impl PartialEq for ClientProps {
fn eq(&self, other: &Self) -> bool {
self.client_id == other.client_id
}
}
impl ClientProps {
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
ClientProps { client_id, client }
}
}

View file

@ -1 +0,0 @@
pub(crate) mod client;

View file

@ -1,16 +0,0 @@
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
use dioxus::prelude::*;
#[derive(Routable, Clone)]
pub enum Route {
#[layout(AuthHeader)]
#[route("/")]
Home {},
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
#[route("/login?:query_string")]
Login { query_string: String },
#[end_layout]
#[route("/:..route")]
NotFound { route: Vec<String> },
}

View file

@ -1,35 +0,0 @@
use dioxus::prelude::*;
use dioxus_sdk::storage::*;
use crate::{
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
oidc::{AuthRequestState, AuthTokenState},
};
pub fn use_auth_token_provider() {
let stored_token =
use_storage::<LocalStorage, _>(DIOXUS_FRONT_AUTH_TOKEN.to_owned(), AuthTokenState::default);
use_context_provider(move || stored_token);
}
pub fn use_auth_token() -> Signal<AuthTokenState> {
use_context()
}
pub fn use_auth_request_provider() {
let stored_req = use_storage::<LocalStorage, _>(
DIOXUS_FRONT_AUTH_REQUEST.to_owned(),
AuthRequestState::default,
);
use_context_provider(move || stored_req);
}
pub fn use_auth_request() -> Signal<AuthRequestState> {
use_context()
}
pub fn auth_request() -> Signal<AuthRequestState> {
consume_context()
}

View file

@ -1,221 +0,0 @@
use crate::storage::{auth_request, use_auth_request, use_auth_token};
use crate::{
oidc::{
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
AuthRequestState, AuthTokenState, ClientState,
},
props::client::ClientProps,
router::Route,
CLIENT,
};
use anyhow::Result;
use dioxus::prelude::*;
use dioxus::router::prelude::{Link, Outlet};
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
#[component]
pub fn LogOut() -> Element {
let mut auth_token = use_auth_token();
let log_out_url_state = use_signal(|| None::<Option<Result<Url>>>);
match auth_token().id_token {
Some(id_token) => match &*log_out_url_state.read() {
Some(log_out_url_result) => match log_out_url_result {
Some(uri) => match uri {
Ok(uri) => {
rsx! {
Link {
onclick: move |_| {
auth_token.take();
},
to: uri.to_string(),
"Log out"
}
}
}
Err(error) => {
rsx! { div { "Failed to load disconnection url: {error:?}" } }
}
},
None => {
rsx! { div { "Loading... Please wait" } }
}
},
None => {
let logout_url_task = move || {
spawn({
let mut log_out_url_state = log_out_url_state.to_owned();
async move {
let logout_url = log_out_url(id_token).await;
let logout_url_option = Some(logout_url);
log_out_url_state.set(Some(logout_url_option));
}
})
};
logout_url_task();
rsx! { div { "Loading log out url... Please wait" } }
}
},
None => {
rsx! {{}}
}
}
}
#[component]
pub fn RefreshToken(props: ClientProps) -> Element {
let mut auth_token = use_auth_token();
match auth_token().refresh_token {
Some(refresh_token) => {
rsx! { div {
onmounted: {
move |_| {
let client = props.client.clone();
let refresh_token = refresh_token.clone();
async move {
let exchange_refresh_token =
exchange_refresh_token(client, refresh_token).await;
match exchange_refresh_token {
Ok(response_token) => {
auth_token.set(AuthTokenState {
id_token: response_token.id_token().cloned(),
refresh_token: response_token.refresh_token().cloned(),
});
}
Err(_error) => {
auth_token.take();
auth_request().take();
}
}
}
}
},
"Refreshing session, please wait"
} }
}
None => {
rsx! { div { "Id token expired and no refresh token found" } }
}
}
}
#[component]
pub fn LoadClient() -> Element {
let init_client_future = use_resource(move || async move { init_oidc_client().await });
match &*init_client_future.read_unchecked() {
Some(Ok((client_id, client))) => rsx! {
div {
onmounted: {
let client_id = client_id.clone();
let client = client.clone();
move |_| {
*CLIENT.write() = ClientState {
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
};
}
},
"Client successfully loaded"
}
Outlet::<Route> {}
},
Some(Err(error)) => {
log::info! {"Failed to load client: {:?}", error};
rsx! {
div { "Failed to load client: {error:?}" }
Outlet::<Route> {}
}
}
None => {
rsx! {
div {
div { "Loading client, please wait" }
Outlet::<Route> {}
}
}
}
}
}
#[component]
pub fn AuthHeader() -> Element {
let client = CLIENT.read().oidc_client.clone();
let mut auth_request = use_auth_request();
let auth_token = use_auth_token();
match (client, auth_request(), auth_token()) {
// We have everything we need to attempt to authenticate the user
(Some(client_props), current_auth_request, current_auth_token) => {
match current_auth_request.auth_request {
Some(new_auth_request) => {
match current_auth_token.id_token {
Some(id_token) => {
match email(
client_props.client.clone(),
id_token.clone(),
new_auth_request.nonce.clone(),
) {
Ok(email) => {
rsx! {
div {
div { {email} }
LogOut {}
Outlet::<Route> {}
}
}
}
// Id token failed to be decoded
Err(error) => match error {
// Id token failed to be decoded because it expired, we refresh it
openidconnect::ClaimsVerificationError::Expired(_message) => {
log::info!("Token expired");
rsx! {
div {
RefreshToken { client_id: client_props.client_id, client: client_props.client }
Outlet::<Route> {}
}
}
}
// Other issue with token decoding
_ => {
log::info!("Other issue with token");
rsx! {
div {
div { "{error}" }
Outlet::<Route> {}
}
}
}
},
}
}
// User is not logged in
None => {
rsx! {
div {
Link { to: new_auth_request.authorize_url.clone(), "Log in" }
Outlet::<Route> {}
}
}
}
}
}
None => {
rsx! { div {
onmounted: {
let client = client_props.client;
move |_| {
let new_auth_request = authorize_url(client.clone());
auth_request.set(AuthRequestState {
auth_request: Some(new_auth_request),
});
}
},
"Loading nonce"
} }
}
}
}
// Client is not initialized yet, we need it for everything
(None, _, _) => {
rsx! { LoadClient {} }
}
}
}

View file

@ -1,5 +0,0 @@
use dioxus::prelude::*;
pub fn Home() -> Element {
rsx! { div { "Hello world" } }
}

View file

@ -1,86 +0,0 @@
use crate::{
oidc::{token_response, AuthTokenState},
router::Route,
storage::{auth_request, use_auth_token},
CLIENT,
};
use dioxus::prelude::*;
use dioxus::router::prelude::Link;
use openidconnect::{OAuth2TokenResponse, TokenResponse};
#[component]
pub fn Login(query_string: String) -> Element {
let client = CLIENT.read().oidc_client.clone();
let mut auth_token = use_auth_token();
let current_auth_token = auth_token();
match client {
Some(client_props) => {
match (
current_auth_token.id_token,
current_auth_token.refresh_token,
) {
(Some(_id_token), Some(_refresh_token)) => {
rsx! {
div { "Sign in successful" }
Link { to: Route::Home {}, "Go back home" }
}
}
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
(None, Some(_)) | (Some(_), None) => {
rsx! {
div { "Error while attempting to log in" }
Link {
to: Route::Home {},
onclick: move |_| {
auth_token.take();
auth_request().take();
},
"Go back home"
}
}
}
(None, None) => {
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
let code_pair = query_pairs.find(|(key, _value)| key == "code");
match code_pair {
Some((_key, code)) => {
let code = code.to_string();
rsx! { div {
onmounted: {
move |_| {
let auth_code = code.to_string();
let client_props = client_props.clone();
async move {
let token_response_result =
token_response(client_props.client, auth_code).await;
match token_response_result {
Ok(token_response) => {
let id_token = token_response.id_token().unwrap();
auth_token.set(AuthTokenState {
id_token: Some(id_token.clone()),
refresh_token: token_response
.refresh_token()
.cloned(),
});
}
Err(error) => {
log::warn! {"{error}"};
}
}
}
}
}
}}
}
None => {
rsx! { div { "No code provided" } }
}
}
}
}
}
_ => {
rsx! {{}}
}
}
}

View file

@ -1,4 +0,0 @@
pub(crate) mod header;
pub(crate) mod home;
pub(crate) mod login;
pub(crate) mod not_found;

View file

@ -1,10 +0,0 @@
use dioxus::prelude::*;
#[component]
pub fn NotFound(route: Vec<String>) -> Element {
rsx! {
div{
{route.join("")}
}
}
}

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,23 +0,0 @@
use dioxus::prelude::*;
fn main() {
launch_desktop(app);
}
fn app() -> Element {
let mut state = use_signal(|| 0);
use_future(move || async move {
loop {
state += 1;
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
});
rsx! {
button { onclick: move |_| state.set(0), "reset" }
for _ in 0..10000 {
div { "hello desktop! {state}" }
}
}
}

View file

@ -1,207 +0,0 @@
#![allow(unused)]
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dioxus_ssr::Renderer;
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("build 1000 routes", |b| {
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
b.iter(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| Box::pin(async move {}),
&wrapper,
)
.await
.unwrap();
}
})
})
});
c.bench_function("build 1000 routes no memory cache", |b| {
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.memory_cache_limit(0)
.invalidate_after(Duration::from_secs(10))
.build();
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| Box::pin(async move {}),
&wrapper,
)
.await
.unwrap();
}
})
});
c.bench_function("build 1000 routes no cache", |b| {
let mut renderer = Renderer::default();
b.iter(|| {
for id in 0..1000 {
let mut vdom = VirtualDom::new_with_props(
RenderPath,
RenderPathProps::builder().path(Route::Post { id }).build(),
);
vdom.rebuild_in_place();
struct Ignore;
impl std::fmt::Write for Ignore {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
Ok(())
}
}
renderer.render_to(&mut Ignore, &vdom).unwrap();
}
})
});
c.bench_function("cache static", |b| {
let wrapper = DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
};
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder()
.static_dir("./static")
.build();
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
.await
.unwrap();
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
#[component]
fn Blog() -> Element {
rsx! {
div {
"Blog"
}
}
}
#[component]
fn Post(id: usize) -> Element {
rsx! {
for _ in 0..id {
div {
"PostId: {id}"
}
}
}
}
#[component]
fn PostHome() -> Element {
rsx! {
div {
"Post"
}
}
}
#[component]
fn Home() -> Element {
rsx! {
div {
"Home"
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[route("/")]
Blog {},
#[route("/post/index")]
PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest]
#[route("/")]
Home {},
}
#[component]
fn RenderPath(path: Route) -> Element {
let path = path.clone();
rsx! {
Router::<Route> {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}