--- name: lightfriend-add-frontend-page description: Step-by-step guide for adding new pages to the Yew frontend --- # Adding a New Frontend Page This skill guides you through adding a new page to the Lightfriend Yew WebAssembly frontend. ## Overview A complete page includes: - Page component in `frontend/src/pages/` - Route enum variant in `main.rs` - Route handler in switch function - Navigation link (if applicable) ## Step-by-Step Process ### 1. Create Page Component Create `frontend/src/pages/{page_name}.rs`: ```rust use yew::prelude::*; use gloo_net::http::Request; use crate::config; #[function_component(PageName)] pub fn page_name() -> Html { // State management let data = use_state(|| None::); let loading = use_state(|| true); let error = use_state(|| None::); // Load data on mount { let data = data.clone(); let loading = loading.clone(); let error = error.clone(); use_effect_with((), move |_| { wasm_bindgen_futures::spawn_local(async move { match fetch_data().await { Ok(result) => { data.set(Some(result)); loading.set(false); } Err(e) => { error.set(Some(e.to_string())); loading.set(false); } } }); }); } html! {

{"Page Title"}

if *loading {

{"Loading..."}

} else if let Some(err) = (*error).clone() {

{err}

} else if let Some(content) = (*data).clone() { // Render content
{format!("Content: {:?}", content)}
}
} } // Helper functions async fn fetch_data() -> Result> { let token = /* get from context or local storage */; let backend_url = config::get_backend_url(); let response = Request::get(&format!("{}/api/endpoint", backend_url)) .header("Authorization", &format!("Bearer {}", token)) .send() .await? .json::() .await?; Ok(response) } #[derive(Clone, serde::Deserialize, serde::Serialize)] struct SomeData { // Define your data structure } ``` ### 2. Add Module Declaration **CRITICAL: This codebase does NOT use `mod.rs` files!** Instead, add the module declaration to the inline `mod pages { }` block in `frontend/src/main.rs`: ```rust mod pages { pub mod home; pub mod landing; pub mod {page_name}; // Add your new page here // ... other pages } ``` **NEVER create a `mod.rs` file** - this is a common mistake. Lightfriend uses named module files (e.g., `home.rs`, `landing.rs`) and declares them in the inline module block in `main.rs`. ### 3. Add Route Variant In `frontend/src/main.rs`, add a route variant to the `Route` enum: ```rust #[derive(Clone, Routable, PartialEq)] pub enum Route { #[at("/")] Home, #[at("/page-name")] PageName, // ... other routes #[not_found] #[at("/404")] NotFound, } ``` ### 4. Add Route Handler In the `switch()` function in `frontend/src/main.rs`, add: ```rust fn switch(route: Route) -> Html { match route { Route::Home => html! { }, Route::PageName => html! { }, // ... other routes Route::NotFound => html! {

{"404 - Page Not Found"}

}, } } ``` ### 5. Add Navigation Link (Optional) If the page should appear in navigation, add to the `Nav` component in `frontend/src/main.rs`: ```rust #[function_component(Nav)] fn nav() -> Html { html! { } } ``` ### 6. Test the Page ```bash cd frontend && trunk serve ``` Navigate to `http://localhost:8080/page-name` ## Common Patterns ### Protected Routes (Require Auth) ```rust use yew_hooks::use_local_storage; #[function_component(ProtectedPage)] pub fn protected_page() -> Html { let token = use_local_storage::("token".to_string()); if token.is_none() { // Redirect to login let navigator = use_navigator().unwrap(); navigator.push(&Route::Login); return html! {}; } html! {
{"Protected content"}
} } ``` ### Page with Form ```rust use web_sys::HtmlInputElement; #[function_component(FormPage)] pub fn form_page() -> Html { let name_ref = use_node_ref(); let email_ref = use_node_ref(); let submitting = use_state(|| false); let on_submit = { let name_ref = name_ref.clone(); let email_ref = email_ref.clone(); let submitting = submitting.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); let submitting = submitting.clone(); let name = name_ref.cast::() .unwrap() .value(); let email = email_ref.cast::() .unwrap() .value(); submitting.set(true); wasm_bindgen_futures::spawn_local(async move { match submit_form(name, email).await { Ok(_) => { // Handle success } Err(e) => { // Handle error } } submitting.set(false); }); }) }; html! {
} } async fn submit_form(name: String, email: String) -> Result<(), Box> { let token = /* get from context */; Request::post(&format!("{}/api/submit", config::get_backend_url())) .header("Authorization", &format!("Bearer {}", token)) .json(&serde_json::json!({ "name": name, "email": email, }))? .send() .await?; Ok(()) } ``` ### Page with Context ```rust use yew::prelude::*; #[derive(Clone, PartialEq)] pub struct UserContext { pub user_id: i32, pub email: String, } #[function_component(ContextPage)] pub fn context_page() -> Html { let user_ctx = use_context::() .expect("UserContext not found"); html! {

{format!("User ID: {}", user_ctx.user_id)}

{format!("Email: {}", user_ctx.email)}

} } ``` ### Page with Route Parameters ```rust #[derive(Clone, Routable, PartialEq)] pub enum Route { #[at("/users/:id")] UserDetail { id: i32 }, } #[derive(Properties, PartialEq)] pub struct UserDetailProps { pub id: i32, } #[function_component(UserDetail)] pub fn user_detail(props: &UserDetailProps) -> Html { let user_data = use_state(|| None::); { let user_data = user_data.clone(); let user_id = props.id; use_effect_with(user_id, move |_| { wasm_bindgen_futures::spawn_local(async move { if let Ok(user) = fetch_user(user_id).await { user_data.set(Some(user)); } }); }); } html! {
if let Some(user) = (*user_data).clone() {

{user.name}

}
} } // In switch function: fn switch(route: Route) -> Html { match route { Route::UserDetail { id } => html! { }, // ... } } ``` ### Page with Multiple API Calls ```rust #[function_component(DashboardPage)] pub fn dashboard_page() -> Html { let stats = use_state(|| None::); let activity = use_state(|| None::>); let loading = use_state(|| true); { let stats = stats.clone(); let activity = activity.clone(); let loading = loading.clone(); use_effect_with((), move |_| { wasm_bindgen_futures::spawn_local(async move { // Fetch multiple endpoints in parallel let (stats_result, activity_result) = tokio::join!( fetch_stats(), fetch_activity() ); if let Ok(s) = stats_result { stats.set(Some(s)); } if let Ok(a) = activity_result { activity.set(Some(a)); } loading.set(false); }); }); } html! {
if *loading {

{"Loading dashboard..."}

} else {
{render_stats(&stats)} {render_activity(&activity)}
}
} } ``` ## Styling **Lightfriend uses CSS style blocks within the `html!` macro, NOT inline Tailwind classes.** Common pattern: ```rust html! {

{"Title"}

{"Card content"}
} ``` ## Testing Checklist - [ ] Page renders without errors - [ ] Route works in browser - [ ] Navigation link works (if added) - [ ] API calls succeed - [ ] Loading states display correctly - [ ] Error states display correctly - [ ] Authentication checks work (if protected) - [ ] Mobile responsive (if applicable) ## File Reference - Page components: `frontend/src/pages/{page}.rs` - Routes: `frontend/src/main.rs` (Route enum + switch function) - Navigation: `frontend/src/main.rs` (Nav component) - Shared components: `frontend/src/components/` - Backend config: `frontend/src/config.rs`