Rustelo/client/src/app.rs
2025-07-07 23:05:46 +01:00

295 lines
24 KiB
Rust

//#![allow(unused_imports)]
//#![allow(dead_code)]
//#![allow(unused_variables)]
// Suppress leptos_router warnings about reactive signal access outside tracking context
#![allow(clippy::redundant_closure)]
//#![allow(unused_assignments)]
//use crate::defs::{NAV_LINK_CLASS, ROUTES};
use crate::auth::AuthProvider;
use crate::components::NavbarLogo;
use crate::i18n::{I18nProvider, LanguageSelector, use_i18n};
use crate::pages::{AboutPage, DaisyUIPage, FeaturesDemoPage, HomePage};
use crate::state::*;
use crate::utils::{get_initial_path, make_navigate, make_on_link_click, make_popstate_effect};
use leptos::children::Children;
use leptos::prelude::*;
use leptos_meta::{MetaTags, Title, provide_meta_context};
// use regex::Regex;
use shared::{get_bundle, load_menu_toml, t};
use std::collections::HashMap;
//// Wrapper component for consistent layout.
#[component]
fn Wrapper(children: Children) -> impl IntoView {
view! { <>{children()}</> }
}
/// NotFoundPage component for 404s.
#[component]
fn NotFoundPage() -> impl IntoView {
view! { <div class="text-center">"Page not found."</div> }
}
/// Navigation menu component, maps over ROUTES.
#[component]
pub fn NavMenu(path: ReadSignal<String>, set_path: WriteSignal<String>) -> impl IntoView {
let navigate = make_navigate(set_path.clone());
let on_link_click = make_on_link_click(set_path.clone(), navigate.clone());
let i18n = use_i18n();
let menu_items = load_menu_toml().unwrap_or_default();
println!("NavMenu rendered");
view! {
<nav class="rounded-lg border shadow-lg overflow-hidden p-2 bg-white border-stone-200 shadow-stone-950/5 mx-auto w-full max-w-screen-xl">
<div class="flex items-center">
<NavbarLogo size="small".to_string() />
<hr class="ml-1 mr-1.5 hidden h-5 w-px border-l border-t-0 border-secondary-dark lg:block" />
<div class="hidden lg:block">
<ul class="mt-4 flex flex-col gap-x-3 gap-y-1.5 lg:mt-0 lg:flex-row lg:items-center">
{menu_items.menu.into_iter().map(|item| {
let on_link_click = on_link_click.clone();
let route = item.route.clone();
let route_for_click = route.clone();
let route_for_aria = route.clone();
let lang_val = i18n.lang_code();
let label = match lang_val.as_str() {
"es" => item.label.es.clone(),
_ => item.label.en.clone(),
};
view! {
<li><a
href={route.clone()}
on:click=move |ev| on_link_click(ev, Box::leak(route_for_click.clone().into_boxed_str()))
class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"
aria-current=move || if path.get() == route_for_aria { Some("page") } else { None as Option<&'static str> }
>
<svg width="1.5em" height="1.5em" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M7 18H10.5H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 14H7.5H8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 10H8.5H10" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 2L16.5 2L21 6.5V19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 20.5V6.5C3 5.67157 3.67157 5 4.5 5H14.2515C14.4106 5 14.5632 5.06321 14.6757 5.17574L17.8243 8.32426C17.9368 8.43679 18 8.5894 18 8.74853V20.5C18 21.3284 17.3284 22 16.5 22H4.5C3.67157 22 3 21.3284 3 20.5Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 5V8.4C14 8.73137 14.2686 9 14.6 9H18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
{label}</a>
</li>
}
}).collect_view()}
<li>
<a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary">
<svg width="1.5em" height="1.5em" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M7 18H10.5H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 14H7.5H8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 10H8.5H10" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 2L16.5 2L21 6.5V19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 20.5V6.5C3 5.67157 3.67157 5 4.5 5H14.2515C14.4106 5 14.5632 5.06321 14.6757 5.17574L17.8243 8.32426C17.9368 8.43679 18 8.5894 18 8.74853V20.5C18 21.3284 17.3284 22 16.5 22H4.5C3.67157 22 3 21.3284 3 20.5Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 5V8.4C14 8.73137 14.2686 9 14.6 9H18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
{i18n.t("pages")}</a>
</li>
<li>
<LanguageSelector class="ml-2".to_string() />
</li>
// <li>
// <a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M4.271 18.3457C4.271 18.3457 6.50002 15.5 12 15.5C17.5 15.5 19.7291 18.3457 19.7291 18.3457" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 12C13.6569 12 15 10.6569 15 9C15 7.34315 13.6569 6 12 6C10.3431 6 9 7.34315 9 9C9 10.6569 10.3431 12 12 12Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>Account</a>
// </li>
// <li>
// <a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M21 7.35304L21 16.647C21 16.8649 20.8819 17.0656 20.6914 17.1715L12.2914 21.8381C12.1102 21.9388 11.8898 21.9388 11.7086 21.8381L3.30861 17.1715C3.11814 17.0656 3 16.8649 3 16.647L2.99998 7.35304C2.99998 7.13514 3.11812 6.93437 3.3086 6.82855L11.7086 2.16188C11.8898 2.06121 12.1102 2.06121 12.2914 2.16188L20.6914 6.82855C20.8818 6.93437 21 7.13514 21 7.35304Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3.52844 7.29357L11.7086 11.8381C11.8898 11.9388 12.1102 11.9388 12.2914 11.8381L20.5 7.27777" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 21L12 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11.6914 11.8285L3.89139 7.49521C3.49147 7.27304 3 7.56222 3 8.01971V16.647C3 16.8649 3.11813 17.0656 3.30861 17.1715L11.1086 21.5048C11.5085 21.727 12 21.4378 12 20.9803V12.353C12 12.1351 11.8819 11.9344 11.6914 11.8285Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"></path></svg>Blocks</a>
// </li>
// <li>
// <a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M7 6L17 6" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 9L17 9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9 17H15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 12H2.6C2.26863 12 2 12.2686 2 12.6V21.4C2 21.7314 2.26863 22 2.6 22H21.4C21.7314 22 22 21.7314 22 21.4V12.6C22 12.2686 21.7314 12 21.4 12H21M3 12V2.6C3 2.26863 3.26863 2 3.6 2H20.4C20.7314 2 21 2.26863 21 2.6V12M3 12H21" stroke="currentColor"></path></svg>Docs</a>
// </li>
</ul>
</div>
<div class="ml-auto flex items-center space-x-2">
<LanguageSelector />
<div class="w-40">
<div class="relative w-full">
<input placeholder="Search here..." type="search" class="w-full aria-disabled:cursor-not-allowed outline-none focus:outline-none text-stone-800 dark:text-white placeholder:text-stone-600/60 ring-transparent border border-stone-200 transition-all ease-in disabled:opacity-50 disabled:pointer-events-none select-none text-sm py-1.5 pl-8 pr-2 ring shadow-sm bg-white rounded-lg duration-100 hover:border-stone-300 hover:ring-none focus:border-stone-400 focus:ring-none peer" />
<span class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-stone-600/70 peer-focus:text-stone-800 peer-focus:text-stone-800 dark:peer-hover:text-white dark:peer-focus:text-white transition-all duration-300 ease-in overflow-hidden w-4 h-4"><svg width="1.5em" height="1.5em" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-full w-full"><path d="M17 17L21 21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 11C3 15.4183 6.58172 19 11 19C13.213 19 15.2161 18.1015 16.6644 16.6493C18.1077 15.2022 19 13.2053 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</span>
</div>
</div>
</div>
<button data-dui-toggle="collapse" data-dui-target="#navbar-collapse-search" aria-expanded="false" aria-controls="navbar-collapse-search" class="place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none text-sm min-w-[34px] min-h-[34px] rounded-md bg-transparent border-transparent text-stone-800 hover:bg-stone-800/5 hover:border-stone-800/5 shadow-none hover:shadow-none ml-1 grid lg:hidden"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M3 5H21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 12H21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 19H21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</button>
</div>
<div class="overflow-hidden transition-[max-height] duration-300 ease-in-out max-h-0 lg:hidden" id="navbar-collapse-search">
<ul class="flex flex-col gap-0.5 mt-2">
<li>
<a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M7 18H10.5H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 14H7.5H8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 10H8.5H10" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 2L16.5 2L21 6.5V19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 20.5V6.5C3 5.67157 3.67157 5 4.5 5H14.2515C14.4106 5 14.5632 5.06321 14.6757 5.17574L17.8243 8.32426C17.9368 8.43679 18 8.5894 18 8.74853V20.5C18 21.3284 17.3284 22 16.5 22H4.5C3.67157 22 3 21.3284 3 20.5Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 5V8.4C14 8.73137 14.2686 9 14.6 9H18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>{i18n.t("pages")}</a>
</li>
<li>
<a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M4.271 18.3457C4.271 18.3457 6.50002 15.5 12 15.5C17.5 15.5 19.7291 18.3457 19.7291 18.3457" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 12C13.6569 12 15 10.6569 15 9C15 7.34315 13.6569 6 12 6C10.3431 6 9 7.34315 9 9C9 10.6569 10.3431 12 12 12Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>Account</a>
</li>
<li>
<a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M21 7.35304L21 16.647C21 16.8649 20.8819 17.0656 20.6914 17.1715L12.2914 21.8381C12.1102 21.9388 11.8898 21.9388 11.7086 21.8381L3.30861 17.1715C3.11814 17.0656 3 16.8649 3 16.647L2.99998 7.35304C2.99998 7.13514 3.11812 6.93437 3.3086 6.82855L11.7086 2.16188C11.8898 2.06121 12.1102 2.06121 12.2914 2.16188L20.6914 6.82855C20.8818 6.93437 21 7.13514 21 7.35304Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3.52844 7.29357L11.7086 11.8381C11.8898 11.9388 12.1102 11.9388 12.2914 11.8381L20.5 7.27777" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 21L12 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11.6914 11.8285L3.89139 7.49521C3.49147 7.27304 3 7.56222 3 8.01971V16.647C3 16.8649 3.11813 17.0656 3.30861 17.1715L11.1086 21.5048C11.5085 21.727 12 21.4378 12 20.9803V12.353C12 12.1351 11.8819 11.9344 11.6914 11.8285Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"></path></svg>Blocks</a>
</li>
<li>
<a href="#" class="font-sans antialiased text-sm text-current flex items-center gap-x-2 p-1 hover:text-primary"><svg width="1.5em" height="1.5em" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-4 w-4"><path d="M7 6L17 6" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 9L17 9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9 17H15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 12H2.6C2.26863 12 2 12.2686 2 12.6V21.4C2 21.7314 2.26863 22 2.6 22H21.4C21.7314 22 22 21.7314 22 21.4V12.6C22 12.2686 21.7314 12 21.4 12H21M3 12V2.6C3 2.26863 3.26863 2 3.6 2H20.4C20.7314 2 21 2.26863 21 2.6V12M3 12H21" stroke="currentColor"></path></svg>Docs</a>
</li>
</ul>
</div>
</nav>
}
// view! {
// <nav class="bg-white shadow-sm border-b">
// <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
// <div class="flex justify-between h-16">
// <div class="flex items-center">
// <a href="/" on:click={ let on_link_click = on_link_click.clone(); move |ev| on_link_click(ev, "/") } class="text-xl font-bold text-gray-900">"Leptos App"</a>
// </div>
// <div class="flex items-center space-x-4">
// //{ROUTES.iter().map(|(route, label)| {
// {menu_items.menu.into_iter().map(|item| {
// let on_link_click = on_link_click.clone();
// let route = item.route.clone();
// let route_for_click = route.clone();
// let route_for_aria = route.clone();
// let lang_val = lang.get();
// let label = match lang_val.as_str() {
// "es" => item.label.es.as_str(),
// _ => item.label.en.as_str(),
// };
// view! {
// <a
// href={route.as_str()}
// on:click=move |ev| on_link_click(ev, Box::leak(route_for_click.clone().into_boxed_str()))
// class=NAV_LINK_CLASS
// aria-current=move || if path.get() == route_for_aria { Some("page") } else { None }
// >{label}</a>
// }
// }).collect_view()}
// { view! {
// <div class=move || if path.get() != "/" {
// "flex flex-1 justify-end"
// } else {
// "hidden"
// }>
// <a href="/" on:click=move |ev| on_link_click(ev, "/")
// aria-current=move || Some("page")
// >
// <span class="-m-1.5 text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-300 border border-dashed rounded-xl px-4 py-2 opacity-50 hover:opacity-100 transition-all duration-300">Home</span>
// </a>
// </div>
// }}
// </div>
// </div>
// </div>
// </nav>
//}
}
/// Main app component with SSR path awareness and SPA routing.
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
let (path, set_path) = signal(get_initial_path());
make_popstate_effect(set_path);
let (lang, _set_lang) = signal("en".to_string());
// --- Unit test placeholder for route matching ---
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn test_user_route() {
// let re = Regex::new(r"^/user/(\\d+)$").expect("Valid regex");
// assert!(re.is_match("/user/42"));
// }
// }
view! {
<GlobalStateProvider>
<ThemeProvider>
<I18nProvider>
<ToastProvider>
<AuthProvider>
<UserProvider>
<AppStateProvider>
<Title text="Welcome to Leptos"/>
<header class="absolute inset-x-0 top-0 z-50">
<Wrapper><NavMenu path=path set_path=set_path /></Wrapper>
</header>
<div class="min-h-screen bg-gray-50">
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
{ let lang = lang.clone(); let path = path.clone();
move || {
let p = path.get();
let lang_val = lang.get();
let bundle = get_bundle(&lang_val).unwrap_or_else(|_| {
// Fallback to a simple bundle if loading fails
use fluent::FluentBundle;
use unic_langid::LanguageIdentifier;
let langid: LanguageIdentifier = "en".parse().unwrap_or_else(|e| {
web_sys::console::error_1(&format!("Failed to parse default language 'en': {:?}", e).into());
// This should never happen, but create a minimal fallback
LanguageIdentifier::from_parts(
unic_langid::subtags::Language::from_bytes(b"en").unwrap_or_else(|e| {
web_sys::console::error_1(&format!("Critical error: failed to create 'en' language: {:?}", e).into());
// Fallback to creating a new language identifier from scratch
match "en".parse::<unic_langid::subtags::Language>() {
Ok(lang) => lang,
Err(_) => {
// If even this fails, we'll use the default language
web_sys::console::error_1(&"Using default language as final fallback".into());
unic_langid::subtags::Language::default()
}
}
}),
None,
None,
&[],
)
});
FluentBundle::new(vec![langid])
});
let content = match p.as_str() {
"/" => t(&bundle, "main-desc", None),
"/about" => t(&bundle, "about-desc", None),
"/daisyui" => "DaisyUI Components Demo".to_string(),
"/features-demo" => "New Features Demo".to_string(),
_ if p.starts_with("/user/") => {
if let Some(id) = p.strip_prefix("/user/") {
let mut args = HashMap::new();
args.insert("id", id);
t(&bundle, "user-page", Some(&args))
} else {
t(&bundle, "not-found", None)
}
},
_ => t(&bundle, "not-found", None),
};
view! {
<Wrapper>
<div>{content}</div>
{match p.as_str() {
"/" => view! { <div><HomePage /></div> }.into_any(),
"/about" => view! { <div><AboutPage /></div> }.into_any(),
"/daisyui" => view! { <div><DaisyUIPage /></div> }.into_any(),
"/features-demo" => view! { <div><FeaturesDemoPage /></div> }.into_any(),
_ => view! { <div>Not found</div> }.into_any(),
}}
</Wrapper>
}
}}
</main>
</div>
</AppStateProvider>
</UserProvider>
</AuthProvider>
</ToastProvider>
</I18nProvider>
</ThemeProvider>
</GlobalStateProvider>
}
}
/// The SSR shell for Leptos/Axum integration.
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<link rel="stylesheet" id="leptos" href="/pkg/website.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
<MetaTags/>
</head>
<body>
<App />
</body>
</html>
}
}