feat(examples): add workout copilot exemplar
Add a runnable phone-first workout example that keeps local workout truth as commands, domain events, and projections, routes export through the host capability boundary, and documents how to open it in the browser. req: examples/001 req: local/001 req: local/003 req: local/004 req: host/001 req: host/005
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
use axum::extract::State;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use hemx_axum::{
|
||||
interactions, runtime_js, runtime_js_path, EffectResponse, HandlerRegistry, InteractionRequest,
|
||||
};
|
||||
use hemx_workout_example::ui::workout;
|
||||
use hemx_workout_example::{self as workout_app, AppState};
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = app(AppState::demo());
|
||||
let addr = std::env::var("HEMX_WORKOUT_ADDR")
|
||||
.ok()
|
||||
.and_then(|value| SocketAddr::from_str(&value).ok())
|
||||
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 3028)));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.expect(
|
||||
"bind workout example address; set HEMX_WORKOUT_ADDR=127.0.0.1:3030 if the default is busy",
|
||||
);
|
||||
eprintln!("hemx workout example: http://{addr}");
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
fn app(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(page).post(interact))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn page(State(state): State<AppState>) -> impl IntoResponse {
|
||||
axum::response::Html(state.with_workout(|workout| workout_app::render(workout).into_string()))
|
||||
}
|
||||
|
||||
async fn interact(
|
||||
State(state): State<AppState>,
|
||||
request: InteractionRequest,
|
||||
) -> Result<EffectResponse, impl IntoResponse> {
|
||||
request.dispatch_async(registry(state)).await
|
||||
}
|
||||
|
||||
async fn runtime() -> impl IntoResponse {
|
||||
runtime_js()
|
||||
}
|
||||
|
||||
fn registry(state: AppState) -> HandlerRegistry {
|
||||
interactions(hemx_workout_example::ui::BUILD_FINGERPRINT)
|
||||
.on(workout::complete_set, {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::complete_set(&state)
|
||||
})
|
||||
.on(workout::change_weight, {
|
||||
let state = state.clone();
|
||||
move |form| workout_app::change_weight(&state, form.parse::<f32>("kg").unwrap_or(0.0))
|
||||
})
|
||||
.on(workout::skip_exercise, {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::skip_exercise(&state)
|
||||
})
|
||||
.on(workout::record_note, {
|
||||
let state = state.clone();
|
||||
move |form| {
|
||||
workout_app::record_note(&state, form.value("text").unwrap_or("").to_owned())
|
||||
}
|
||||
})
|
||||
.on(workout::export_log, {
|
||||
let state = state.clone();
|
||||
move |_| workout_app::export_log(&state)
|
||||
})
|
||||
.on(workout::record_share_result, move |_| {
|
||||
workout_app::record_share_result(&state)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn routes_can_be_built_for_run_and_deploy_smoke() {
|
||||
let _app = app(AppState::demo());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user