feat(examples): add saas tutorial skeleton
Add a compile-tested v1 tutorial skeleton that proves the production-shaped app boundary without provider-heavy scope: auth/session context, CSRF-checked mutation, local persistence adapter, generated form/slot/keyed row/page/push effects, plain CSS, SSE shape, and one explicit metrics island. req: examples/001 req: auth/001 req: auth/002 req: auth/004 req: form/001 req: failure/004 req: page_swap/002 req: push/003
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use futures_util::stream;
|
||||
use hemx::IntoEffect;
|
||||
use hemx_axum::{runtime_js, sse, EffectResponse, InteractionRequest};
|
||||
use hemx_saas_example::{home_page, live_status, registry, ui, AppContext};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = app(AppContext::demo());
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3003")
|
||||
.await
|
||||
.expect("bind saas tutorial example");
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("serve saas tutorial example");
|
||||
}
|
||||
|
||||
fn app(ctx: AppContext) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/settings", get(settings))
|
||||
.route("/events", get(events))
|
||||
.route("/hemx.js", get(runtime))
|
||||
.route("/app.css", get(css))
|
||||
.route("/metrics.js", get(metrics_js))
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
||||
async fn home(State(ctx): State<AppContext>) -> impl IntoResponse {
|
||||
axum::response::Html(home_page(&ctx).into_string())
|
||||
}
|
||||
|
||||
async fn settings(State(ctx): State<AppContext>) -> impl IntoResponse {
|
||||
axum::response::Html(home_page(&ctx).into_string())
|
||||
}
|
||||
|
||||
async fn interact(
|
||||
State(ctx): State<AppContext>,
|
||||
request: InteractionRequest,
|
||||
) -> Result<EffectResponse, impl IntoResponse> {
|
||||
request.dispatch_async(registry(ctx)).await
|
||||
}
|
||||
|
||||
async fn events(
|
||||
Query(params): Query<BTreeMap<String, String>>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> impl IntoResponse {
|
||||
let count = ctx.projects().len();
|
||||
if params.contains_key("once") {
|
||||
return sse(stream::iter([Ok::<_, Infallible>(
|
||||
live_status(count).into_batch(ui::BUILD_FINGERPRINT),
|
||||
)]));
|
||||
}
|
||||
|
||||
sse(stream::iter([Ok::<_, Infallible>(
|
||||
live_status(count).into_batch(ui::BUILD_FINGERPRINT),
|
||||
)]))
|
||||
}
|
||||
|
||||
async fn runtime() -> impl IntoResponse {
|
||||
runtime_js()
|
||||
}
|
||||
|
||||
async fn css() -> Response {
|
||||
Response::builder()
|
||||
.header("content-type", "text/css; charset=utf-8")
|
||||
.body(Body::from(include_str!("../templates/app.css")))
|
||||
.expect("css response")
|
||||
}
|
||||
|
||||
async fn metrics_js() -> Response {
|
||||
Response::builder()
|
||||
.header("content-type", "text/javascript; charset=utf-8")
|
||||
.body(Body::from(include_str!("../templates/metrics.js")))
|
||||
.expect("metrics js response")
|
||||
}
|
||||
Reference in New Issue
Block a user