mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-24 19:59:50 -04:00
Basic ssr wiring using conditional compilation
This commit is contained in:
parent
104464a912
commit
856fac5ece
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -586,6 +586,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html-escape"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c"
|
||||||
|
dependencies = [
|
||||||
|
"utf8-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
@ -698,6 +707,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
"csv",
|
"csv",
|
||||||
|
"kitchen-wasm",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"recipe-store",
|
"recipe-store",
|
||||||
"recipes",
|
"recipes",
|
||||||
@ -1238,9 +1248,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f5cea65876897bb946a623e16bf3df2de4997a6872d95b99dfaed5dd8e14e264"
|
checksum = "f5cea65876897bb946a623e16bf3df2de4997a6872d95b99dfaed5dd8e14e264"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
|
"html-escape",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lexical",
|
"lexical",
|
||||||
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sycamore-macro",
|
"sycamore-macro",
|
||||||
@ -1509,6 +1521,12 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-width"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -11,6 +11,7 @@ tracing = "0.1.35"
|
|||||||
tracing-subscriber = "0.3.14"
|
tracing-subscriber = "0.3.14"
|
||||||
recipes = { path = "../recipes" }
|
recipes = { path = "../recipes" }
|
||||||
recipe-store = {path = "../recipe-store" }
|
recipe-store = {path = "../recipe-store" }
|
||||||
|
kitchen-wasm = { path= "../web" }
|
||||||
csv = "1.1.1"
|
csv = "1.1.1"
|
||||||
axum = "0.5.13"
|
axum = "0.5.13"
|
||||||
rust-embed="6.4.0"
|
rust-embed="6.4.0"
|
||||||
|
@ -32,11 +32,22 @@ use tracing::{debug, info, instrument};
|
|||||||
#[folder = "../web/dist"]
|
#[folder = "../web/dist"]
|
||||||
struct UiAssets;
|
struct UiAssets;
|
||||||
|
|
||||||
pub struct StaticFile<T>(pub T);
|
pub struct StaticFile<T>(pub T)
|
||||||
|
where
|
||||||
|
T: Into<String> + Clone;
|
||||||
|
|
||||||
|
impl<T> StaticFile<T>
|
||||||
|
where
|
||||||
|
T: Into<String> + Clone,
|
||||||
|
{
|
||||||
|
pub fn exists(&self) -> bool {
|
||||||
|
UiAssets::get(self.0.clone().into().as_str()).is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> IntoResponse for StaticFile<T>
|
impl<T> IntoResponse for StaticFile<T>
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String> + Clone,
|
||||||
{
|
{
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let path = self.0.into();
|
let path = self.0.into();
|
||||||
@ -59,13 +70,18 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn ui_static_assets(Path(path): Path<String>) -> impl IntoResponse {
|
async fn ui_assets(Path(path): Path<String>) -> impl IntoResponse {
|
||||||
info!("Serving ui path");
|
info!("Serving ui path");
|
||||||
|
|
||||||
let mut path = path.trim_start_matches("/");
|
let mut path = path.trim_start_matches("/");
|
||||||
path = if path == "" { "index.html" } else { path };
|
path = if path == "" { "index.html" } else { path };
|
||||||
debug!(path = path, "Serving transformed path");
|
debug!(path = path, "Serving transformed path");
|
||||||
StaticFile(path.to_owned())
|
let file = StaticFile(path.to_owned());
|
||||||
|
if file.exists() {
|
||||||
|
file.into_response()
|
||||||
|
} else {
|
||||||
|
kitchen_wasm::render_to_string(path).into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@ -104,7 +120,7 @@ pub async fn ui_main(recipe_dir_path: PathBuf, listen_socket: SocketAddr) {
|
|||||||
//let dir_path = (&dir_path).clone();
|
//let dir_path = (&dir_path).clone();
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.route("/", get(|| async { Redirect::temporary("/ui/") }))
|
.route("/", get(|| async { Redirect::temporary("/ui/") }))
|
||||||
.route("/ui/*path", get(ui_static_assets))
|
.route("/ui/*path", get(ui_assets))
|
||||||
// recipes api path route
|
// recipes api path route
|
||||||
.route("/api/v1/recipes", get(api_recipes))
|
.route("/api/v1/recipes", get(api_recipes))
|
||||||
// categories api path route
|
// categories api path route
|
||||||
|
@ -3,11 +3,6 @@ name = "kitchen-wasm"
|
|||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
|
||||||
ssr = []
|
|
||||||
web = []
|
|
||||||
default = ["web"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
@ -45,8 +40,8 @@ features = [
|
|||||||
|
|
||||||
[dependencies.sycamore]
|
[dependencies.sycamore]
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
features = ["futures", "serde", "default"]
|
features = ["futures", "serde", "default", "ssr"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
@ -18,19 +18,35 @@ mod router_integration;
|
|||||||
mod service;
|
mod service;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
|
use router_integration::DeriveRoute;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
#[cfg(feature = "web")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use tracing_browser_subscriber;
|
use tracing_browser_subscriber;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
use web::UI;
|
use web::UI;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
if cfg!(feature = "web") {
|
console_error_panic_hook::set_once();
|
||||||
console_error_panic_hook::set_once();
|
tracing_browser_subscriber::configure_as_global_default();
|
||||||
// TODO(jwall): use the tracing_subscriber_browser default setup function when it exists.
|
let root = web_sys::window()
|
||||||
tracing_browser_subscriber::configure_as_global_default();
|
.unwrap()
|
||||||
}
|
.document()
|
||||||
sycamore::render(|| view! { UI() });
|
.unwrap()
|
||||||
|
.query_selector("#sycamore")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sycamore::hydrate_to(|| view! { UI() }, &root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn render_to_string(path: &str) -> String {
|
||||||
|
use app_state::AppRoutes;
|
||||||
|
|
||||||
|
let route = <AppRoutes as DeriveRoute>::from(&(String::new(), path.to_owned(), String::new()));
|
||||||
|
sycamore::render_to_string(|| view! { UI(route) })
|
||||||
}
|
}
|
||||||
|
@ -152,6 +152,27 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StaticRouterProps<R, F, G>
|
||||||
|
where
|
||||||
|
G: GenericNode,
|
||||||
|
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
|
||||||
|
F: Fn(ReadSignal<R>) -> View<G> + 'static,
|
||||||
|
{
|
||||||
|
pub route: R,
|
||||||
|
pub route_select: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(StaticRouter<G>)]
|
||||||
|
pub fn static_router<R, F>(props: StaticRouterProps<R, F, G>) -> View<G>
|
||||||
|
where
|
||||||
|
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
|
||||||
|
F: Fn(ReadSignal<R>) -> View<G> + 'static,
|
||||||
|
{
|
||||||
|
debug!("Setting up static router");
|
||||||
|
(props.route_select)(Signal::new(props.route).handle())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn register_click_handler<G>(view: &View<G>, integration: Rc<BrowserIntegration>)
|
fn register_click_handler<G>(view: &View<G>, integration: Rc<BrowserIntegration>)
|
||||||
where
|
where
|
||||||
|
@ -22,6 +22,7 @@ use sycamore::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[instrument]
|
#[instrument]
|
||||||
fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
||||||
// NOTE(jwall): This needs to not be a dynamic node. The rules around
|
// NOTE(jwall): This needs to not be a dynamic node. The rules around
|
||||||
@ -52,6 +53,36 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
||||||
|
// NOTE(jwall): This needs to not be a dynamic node. The rules around
|
||||||
|
// this are somewhat unclear and underdocumented for Sycamore. But basically
|
||||||
|
// avoid conditionals in the `view!` macro calls here.
|
||||||
|
cloned!((route) => match route.get().as_ref() {
|
||||||
|
AppRoutes::Plan => view! {
|
||||||
|
PlanPage()
|
||||||
|
},
|
||||||
|
AppRoutes::Inventory => view! {
|
||||||
|
InventoryPage()
|
||||||
|
},
|
||||||
|
AppRoutes::Cook => view! {
|
||||||
|
CookPage()
|
||||||
|
},
|
||||||
|
AppRoutes::Recipe(idx) => view! {
|
||||||
|
RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) })
|
||||||
|
},
|
||||||
|
AppRoutes::NotFound => view! {
|
||||||
|
// TODO(Create a real one)
|
||||||
|
PlanPage()
|
||||||
|
},
|
||||||
|
AppRoutes::Error(ref e) => {
|
||||||
|
let e = e.clone();
|
||||||
|
view! {
|
||||||
|
"Error: " (e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn get_appservice() -> AppService<AsyncFileStore> {
|
fn get_appservice() -> AppService<AsyncFileStore> {
|
||||||
@ -62,6 +93,26 @@ fn get_appservice() -> AppService<HttpStore> {
|
|||||||
AppService::new(recipe_store::HttpStore::new("/api/v1".to_owned()))
|
AppService::new(recipe_store::HttpStore::new("/api/v1".to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[component(RouterComponent<G>)]
|
||||||
|
fn rounter_component() -> View<G> {
|
||||||
|
view! {
|
||||||
|
Router(RouterProps{
|
||||||
|
route: AppRoutes::default(),
|
||||||
|
browser_integration: BrowserIntegration::new(),
|
||||||
|
route_select: route_switch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[component(RouterComponent<G>)]
|
||||||
|
fn rounter_component(route: AppRoutes) -> View<G> {
|
||||||
|
view! {
|
||||||
|
StaticRouter(StaticRouterProps{route: route, route_select: route_switch})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[component(UI<G>)]
|
#[component(UI<G>)]
|
||||||
pub fn ui() -> View<G> {
|
pub fn ui() -> View<G> {
|
||||||
@ -94,11 +145,47 @@ pub fn ui() -> View<G> {
|
|||||||
view! {
|
view! {
|
||||||
div(class="app") {
|
div(class="app") {
|
||||||
Header()
|
Header()
|
||||||
Router(RouterProps {
|
RouterComponent()
|
||||||
route: AppRoutes::Plan,
|
}
|
||||||
route_select: route_switch,
|
}
|
||||||
browser_integration: BrowserIntegration::new(),
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[instrument]
|
||||||
|
#[component(UI<G>)]
|
||||||
|
pub fn ui(route: AppRoutes) -> View<G> {
|
||||||
|
let app_service = get_appservice();
|
||||||
|
info!("Rendering UI");
|
||||||
|
view! {
|
||||||
|
// NOTE(jwall): Set the app_service in our toplevel scope. Children will be able
|
||||||
|
// to find the service as long as they are a child of this scope.
|
||||||
|
ContextProvider(ContextProviderProps {
|
||||||
|
value: app_service.clone(),
|
||||||
|
children: || {
|
||||||
|
create_effect(move || {
|
||||||
|
spawn_local_in_scope({
|
||||||
|
let mut app_service = app_service.clone();
|
||||||
|
async move {
|
||||||
|
debug!("fetching recipes");
|
||||||
|
match app_service.fetch_recipes_from_storage() {
|
||||||
|
Ok((_, Some(recipes))) => {
|
||||||
|
app_service.set_recipes(recipes);
|
||||||
|
}
|
||||||
|
Ok((_, None)) => {
|
||||||
|
error!("No recipes to find");
|
||||||
|
}
|
||||||
|
Err(msg) => error!("Failed to get recipes {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
view!{
|
||||||
|
div(class="app") {
|
||||||
|
Header()
|
||||||
|
RouterComponent(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user