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:
1 parent
a9f87384f8
commit
983fcfc616
94 files changed
+2774
-7322
No files matched your search
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-auth-example"
|
||||
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 }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
simple_logger = { version = "4.2.0", optional = true }
|
||||
async-trait = { version = "0.1.71", optional = true }
|
||||
sqlx = { version = "0.7.0", 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 }
|
||||
|
||||
serde = "1.0.159"
|
||||
execute = "0.2.12"
|
||||
anyhow = "1.0.71"
|
||||
|
||||
[dependencies.axum_session]
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "simple_logger", "async-trait", "sqlx", "axum_session", "axum_session_auth", "http", "tower"]
|
||||
web = ["dioxus-web"]
|
||||
@@ -1,262 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
http::Method,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionSqlitePool, SessionStore};
|
||||
use axum_session_auth::*;
|
||||
use core::pin::Pin;
|
||||
use dioxus_fullstack::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};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub anonymous: bool,
|
||||
pub username: String,
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
pub 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> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get_user(userid, pool)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//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(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn create_user_tables(pool: &SqlitePool) {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"anonymous" BOOLEAN NOT NULL,
|
||||
"username" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"token" VARCHAR(256) NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 1, true, 'Guest'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users
|
||||
(id, anonymous, username) SELECT 2, false, 'Test'
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
anonymous = EXCLUDED.anonymous,
|
||||
username = EXCLUDED.username
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_permissions
|
||||
(user_id, token) SELECT 2, 'Category::View'
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[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 struct Session(
|
||||
pub axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>,
|
||||
);
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
type Target = axum_session_auth::AuthSession<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Session {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSessionLayerNotFound;
|
||||
|
||||
impl std::fmt::Display for AuthSessionLayerNotFound {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "AuthSessionLayer was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AuthSessionLayerNotFound {}
|
||||
|
||||
impl IntoResponse for AuthSessionLayerNotFound {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
http::status::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"AuthSessionLayer was not found",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: std::marker::Sync + std::marker::Send> axum::extract::FromRequestParts<S> for Session {
|
||||
type Rejection = AuthSessionLayerNotFound;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
axum_session_auth::AuthSession::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map(Session)
|
||||
.map_err(|_| AuthSessionLayerNotFound)
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx build --features web
|
||||
//! cargo run --features server
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod auth;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
// Hydrate the application on the client
|
||||
dioxus_web::launch::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
use crate::auth::*;
|
||||
use axum::routing::*;
|
||||
use axum_session::SessionConfig;
|
||||
use axum_session::SessionStore;
|
||||
use axum_session_auth::AuthConfig;
|
||||
use axum_session_auth::SessionSqlitePool;
|
||||
simple_logger::SimpleLogger::new().init().unwrap();
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let pool = connect_to_database().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
|
||||
let app = Router::new()
|
||||
// Server side render the application, serve static assets, and register server functions
|
||||
.serve_dioxus_application(ServeConfig::new().unwrap(), app)
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
crate::auth::User,
|
||||
i64,
|
||||
axum_session_auth::SessionSqlitePool,
|
||||
sqlx::SqlitePool,
|
||||
>::new(Some(pool))
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(axum_session::SessionLayer::new(session_store));
|
||||
|
||||
// run it
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
//
|
||||
fn app() -> Element {
|
||||
let mut user_name = use_signal(|| "?".to_string());
|
||||
let mut permissions = use_signal(|| "?".to_string());
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button { onclick: move |_| {
|
||||
async move {
|
||||
login().await.unwrap();
|
||||
}
|
||||
},
|
||||
"Login Test User"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_user_name().await {
|
||||
user_name.set(data);
|
||||
}
|
||||
},
|
||||
"Get User Name"
|
||||
}
|
||||
"User name: {user_name}"
|
||||
}
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_permissions().await {
|
||||
permissions.set(data);
|
||||
}
|
||||
},
|
||||
"Get Permissions"
|
||||
}
|
||||
"Permissions: {permissions}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetUserName)]
|
||||
pub async fn get_user_name() -> Result<String, ServerFnError> {
|
||||
let session: crate::auth::Session = extract().await?;
|
||||
Ok(session.0.current_user.unwrap().username.to_string())
|
||||
}
|
||||
|
||||
#[server(Login)]
|
||||
pub async fn login() -> Result<(), ServerFnError> {
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
auth.login_user(2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(Permissions)]
|
||||
pub async fn get_permissions() -> Result<String, ServerFnError> {
|
||||
let method: axum::http::Method = extract().await?;
|
||||
let auth: crate::auth::Session = extract().await?;
|
||||
let current_user = auth.current_user.clone().unwrap_or_default();
|
||||
|
||||
// lets check permissions only and not worry about if they are anon or not
|
||||
if !axum_session_auth::Auth::<crate::auth::User, i64, sqlx::SqlitePool>::build(
|
||||
[axum::http::Method::POST],
|
||||
false,
|
||||
)
|
||||
.requires(axum_session_auth::Rights::any([
|
||||
axum_session_auth::Rights::permission("Category::View"),
|
||||
axum_session_auth::Rights::permission("Admin::View"),
|
||||
]))
|
||||
.validate(¤t_user, &method, None)
|
||||
.await
|
||||
{
|
||||
return Ok(format!(
|
||||
"User {}, Does not have permissions needed to view this page please login",
|
||||
current_user.username
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"User has Permissions needed. Here are the Users permissions: {:?}",
|
||||
current_user.permissions
|
||||
))
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-desktop-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["launch", "fullstack"] }
|
||||
serde = "1.0.159"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/server"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
@@ -1,43 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// Set the url of the server where server functions are hosted.
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::fullstack::prelude::server_fn::client::set_server_url("http://127.0.0.1:8080");
|
||||
launch(app);
|
||||
}
|
||||
|
||||
pub fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run a server function"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetServerData)]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
/static
|
||||
/dist
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-hackernews-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Evan Almloff <evanalmloff@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
reqwest = { workspace= true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = "0.3.17"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -1,17 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
// Define the Hackernews API and types
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
num::ParseIntError,
|
||||
str::FromStr,
|
||||
};
|
||||
use svg_attributes::to;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
launch(|| rsx! { 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> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
|
||||
rsx! {
|
||||
head::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..."
|
||||
},
|
||||
Stories {}
|
||||
}
|
||||
}
|
||||
div { width: "50%",
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| rsx! {
|
||||
"Loading preview..."
|
||||
},
|
||||
Preview {
|
||||
story
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
})?;
|
||||
|
||||
match stories().unwrap() {
|
||||
Ok(list) => rsx! {
|
||||
div {
|
||||
for story in list {
|
||||
ChildrenOrLoading {
|
||||
key: "{story}",
|
||||
StoryListing { story }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn StoryListing(story: ReadOnlySignal<i64>) -> Element {
|
||||
let story = use_server_future(move || get_story(story()))?;
|
||||
|
||||
let StoryItem {
|
||||
title,
|
||||
url,
|
||||
by,
|
||||
score,
|
||||
time,
|
||||
kids,
|
||||
id,
|
||||
..
|
||||
} = story().unwrap()?.item;
|
||||
|
||||
let url = url.as_deref().unwrap_or_default();
|
||||
let hostname = url
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.trim_start_matches("www.");
|
||||
let score = format!("{score} {}", if score == 1 { " point" } else { " points" });
|
||||
let comments = format!(
|
||||
"{} {}",
|
||||
kids.len(),
|
||||
if kids.len() == 1 {
|
||||
" comment"
|
||||
} else {
|
||||
" comments"
|
||||
}
|
||||
);
|
||||
let time = time.format("%D %l:%M %p");
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
padding: "0.5rem",
|
||||
position: "relative",
|
||||
div { font_size: "1.5rem",
|
||||
Link {
|
||||
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
|
||||
"{title}"
|
||||
}
|
||||
a {
|
||||
color: "gray",
|
||||
href: "https://news.ycombinator.com/from?site={hostname}",
|
||||
text_decoration: "none",
|
||||
" ({hostname})"
|
||||
}
|
||||
}
|
||||
div { display: "flex", flex_direction: "row", color: "gray",
|
||||
div { "{score}" }
|
||||
div { padding_left: "0.5rem", "by {by}" }
|
||||
div { padding_left: "0.5rem", "{time}" }
|
||||
div { padding_left: "0.5rem", "{comments}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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: ReadOnlySignal<PreviewState>) -> Element {
|
||||
let PreviewState {
|
||||
active_story: Some(id),
|
||||
} = story()
|
||||
else {
|
||||
return rsx! {"Hover over a story to preview it here"};
|
||||
};
|
||||
|
||||
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
|
||||
|
||||
let story = story().unwrap()?;
|
||||
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
|
||||
if let Some(text) = &story.item.text { div { dangerous_inner_html: "{text}" } }
|
||||
for comment in story.item.kids.iter().copied() {
|
||||
ChildrenOrLoading {
|
||||
key: "{comment}",
|
||||
Comment { comment }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
}))?;
|
||||
|
||||
let CommentData {
|
||||
by,
|
||||
time,
|
||||
text,
|
||||
id,
|
||||
kids,
|
||||
..
|
||||
} = comment().unwrap()?;
|
||||
|
||||
rsx! {
|
||||
div { padding: "0.5rem",
|
||||
div { color: "gray", "by {by}" }
|
||||
div { dangerous_inner_html: "{text}" }
|
||||
for comment in kids.iter().copied() {
|
||||
ChildrenOrLoading {
|
||||
key: "{comment}",
|
||||
Comment { comment }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static BASE_API_URL: &str = "https://hacker-news.firebaseio.com/v0/";
|
||||
pub static ITEM_API: &str = "item/";
|
||||
pub static USER_API: &str = "user/";
|
||||
const COMMENT_DEPTH: i64 = 1;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StoryPageData {
|
||||
#[serde(flatten)]
|
||||
pub item: StoryItem,
|
||||
#[serde(default)]
|
||||
pub comments: Vec<CommentData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CommentData {
|
||||
pub id: i64,
|
||||
/// there will be no by field if the comment was deleted
|
||||
#[serde(default)]
|
||||
pub by: String,
|
||||
#[serde(default)]
|
||||
pub text: String,
|
||||
#[serde(with = "chrono::serde::ts_seconds")]
|
||||
pub time: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub kids: Vec<i64>,
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StoryItem {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub url: Option<String>,
|
||||
pub text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub by: String,
|
||||
#[serde(default)]
|
||||
pub score: i64,
|
||||
#[serde(default)]
|
||||
pub descendants: i64,
|
||||
#[serde(with = "chrono::serde::ts_seconds")]
|
||||
pub time: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub kids: Vec<i64>,
|
||||
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?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ChildrenOrLoading(children: Element) -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: |context: SuspenseContext| {
|
||||
rsx! {
|
||||
if let Some(placeholder) = context.suspense_placeholder() {
|
||||
{placeholder}
|
||||
} else {
|
||||
LoadingIndicator {}
|
||||
}
|
||||
}
|
||||
},
|
||||
children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn LoadingIndicator() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "spinner",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-hello-world-example"
|
||||
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 = { workspace = true, features = ["fullstack"]}
|
||||
serde = "1.0.159"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = "0.3.17"
|
||||
reqwest = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -1,54 +0,0 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
let server_future = use_server_future(get_server_data)?;
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run a server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
launch(app);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-router-example"
|
||||
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 = { workspace = true, features = ["fullstack", "router"] }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = {workspace = true, features = ["full"], optional = true }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum", "dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
LaunchBuilder::fullstack()
|
||||
.with_cfg(server_only!(ServeConfig::builder().incremental(
|
||||
IncrementalRendererConfig::default()
|
||||
.invalidate_after(std::time::Duration::from_secs(120)),
|
||||
)))
|
||||
.launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog/:id/")]
|
||||
Blog { id: i32 },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog(id: i32) -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..id {
|
||||
tr {
|
||||
for _ in 0..id {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog { id: count() }, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetServerData)]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok("Hello from the server!".to_string())
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
||||
@@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "fullstack-streaming-example"
|
||||
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 = { workspace = true, features = ["fullstack"] }
|
||||
serde = "1.0.159"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = "0.3.17"
|
||||
futures = "0.3.30"
|
||||
tokio = { workspace = true, optional = true }
|
||||
futures-util.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum", "tokio"]
|
||||
web = ["dioxus/web"]
|
||||
@@ -1,41 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use server_fn::codec::{StreamingText, TextStream};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut response = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
response.write().clear();
|
||||
if let Ok(stream) = test_stream().await {
|
||||
response.write().push_str("Stream started\n");
|
||||
let mut stream = stream.into_inner();
|
||||
while let Some(Ok(text)) = stream.next().await {
|
||||
response.write().push_str(&text);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start stream"
|
||||
}
|
||||
"{response}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(output = StreamingText)]
|
||||
pub async fn test_stream() -> Result<TextStream, ServerFnError> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let _ = tx.unbounded_send(Ok("Hello, world!".to_string()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TextStream::new(rx))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
launch(app)
|
||||
}
|
||||
Reference in new issue
Block a user