feat(dioxus): added backend offline error handling, added titles to each page, and now using stylesheet as it removed content preloading before css

This commit is contained in:
darkicewolf50 2025-05-20 17:18:20 -06:00
parent 0ec9ee66f2
commit bff6a997be
14 changed files with 269 additions and 94 deletions

1
Cargo.lock generated
View File

@ -796,6 +796,7 @@ dependencies = [
"dioxus-signals", "dioxus-signals",
"dioxus-web", "dioxus-web",
"manganis", "manganis",
"serde",
"warnings", "warnings",
] ]

View File

@ -19,7 +19,9 @@ web = ["dioxus/web"]
desktop = ["dioxus/desktop"] desktop = ["dioxus/desktop"]
# The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature # The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature
mobile = ["dioxus/mobile"] mobile = ["dioxus/mobile"]
ssg = ["dioxus/fullstack"]
# "fullstack"
[profile] [profile]
[profile.wasm-dev] [profile.wasm-dev]

View File

@ -7,30 +7,122 @@
margin-top: 50px; margin-top: 50px;
} }
#blog a:hover {
color: #91a4d2;
}
#blogs { #blogs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; min-height: 80svh;
align-items: center;
height: 80svh;
} }
#blogs h1 { #blogs h1 {
border-bottom: var(--underlineTitle); border-bottom: var(--underlineTitle);
border-radius: var(--underlineTitleBorderRadius); border-radius: var(--underlineTitleBorderRadius);
margin: 1svh 2svw;
padding: 1svh 2svw;
display: flex;
} }
#blogs button { #blogs button {
/* background-color: transparent; */
background-color: var(--card-background-color); background-color: var(--card-background-color);
border-radius: var(--card-border-radius); border-radius: var(--card-border-radius);
border: none; border: none;
color: #ffffff; color: inherit;
font-size: xx-large; font-size: x-large;
padding: 1rem; padding: 0.5rem;
margin: 1svh 0svw;
}
#blogs p {
margin: 0px;
padding: 0px;
} }
#blogs a { #blogs a {
text-decoration: none; text-decoration: none;
color: inherit;
}
#blogs a:hover {
color: #91a4d2;
}
#blogs-title {
display: flex;
flex-direction: column;
padding-bottom: 1svh;
}
#blogs-title p {
margin: 0svh 1svw;
padding: 1svh 0svw;
}
#blog-loading {
display: flex;
flex-direction: column;
justify-self: center;
align-self: center;
justify-content: center;
align-items: center;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
padding: 2svh 2svw;
margin: 8svh 0svw;
}
#blog-out-of {
display: flex;
flex-direction: column;
justify-self: center;
align-self: center;
justify-content: center;
align-items: center;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
padding: 2svh 2svw;
margin: 8svh 0svw;
}
#blog-nav {
display: flex;
align-items: center;
position: relative;
padding: 0svh 2svw;
margin-top: auto;
margin-bottom: 2svh;
background-color: var(--card-background-color); background-color: var(--card-background-color);
border-radius: var(--card-border-radius); border-radius: var(--card-border-radius);
} }
#blog-nav a:last-child {
margin-left: auto;
}
#blog-nav div {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
justify-self: center;
margin: 1svh 0svw;
padding: 0.5rem;
column-gap: 1svw;
min-height: 28px;
font-size: large;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
position: absolute;
left: 50%;
transform: translateX(-50%);
}

View File

@ -1,6 +1,4 @@
use dioxus::prelude::*; use dioxus::{document, prelude::*};
const EXPERIENCE_CSS: Asset = asset!("/assets/styling/experience.css");
#[component] #[component]
pub fn Experience(professional_jobs: bool) -> Element { pub fn Experience(professional_jobs: bool) -> Element {
@ -10,7 +8,7 @@ pub fn Experience(professional_jobs: bool) -> Element {
}; };
rsx! { rsx! {
div { class: "experience-comp", div { class: "experience-comp",
document::Link { rel: "stylesheet", href: EXPERIENCE_CSS } document::Stylesheet { href: asset!("/assets/styling/experience.css") }
if professional_jobs { if professional_jobs {
h3 { "Professional" } h3 { "Professional" }
} else { } else {
@ -32,7 +30,6 @@ pub fn Experience(professional_jobs: bool) -> Element {
td { "{exp.start_month} - {exp.end_month}" } td { "{exp.start_month} - {exp.end_month}" }
} }
tr { tr {
// td { "" }
td { "{exp.company}" } td { "{exp.company}" }
td { "{exp.location}" } td { "{exp.location}" }
} }

View File

@ -3,8 +3,6 @@ use std::collections::HashMap;
use crate::helper_fun::{tech_table_lookup, TechDes}; use crate::helper_fun::{tech_table_lookup, TechDes};
use dioxus::prelude::*; use dioxus::prelude::*;
const ENDER_CSS: Asset = asset!("/assets/styling/ender.css");
#[component] #[component]
pub fn Ender() -> Element { pub fn Ender() -> Element {
// gets list of items to get // gets list of items to get
@ -16,7 +14,7 @@ pub fn Ender() -> Element {
footer_info.insert(used_tech_item, *tech_table_lookup(used_tech_item)); footer_info.insert(used_tech_item, *tech_table_lookup(used_tech_item));
} }
rsx! { rsx! {
document::Link { rel: "stylesheet", href: ENDER_CSS } document::Stylesheet { href: asset!("/assets/styling/ender.css") }
footer { footer {
p { "Brock Tomlinson © 2025" } p { "Brock Tomlinson © 2025" }
div { div {

View File

@ -1,8 +1,6 @@
use crate::helper_fun::tech_table_lookup; use crate::helper_fun::tech_table_lookup;
use dioxus::prelude::*; use dioxus::prelude::*;
const TECHS_CSS: Asset = asset!("/assets/styling/techs.css");
#[component] #[component]
pub fn TechCard(tech_props: &'static str) -> Element { pub fn TechCard(tech_props: &'static str) -> Element {
let props_tech = tech_table_lookup(tech_props); let props_tech = tech_table_lookup(tech_props);
@ -19,14 +17,12 @@ pub fn TechCard(tech_props: &'static str) -> Element {
#[component] #[component]
pub fn TechCat(cat: &'static str, tech_vec: Vec<&'static str>) -> Element { pub fn TechCat(cat: &'static str, tech_vec: Vec<&'static str>) -> Element {
rsx! { rsx! {
document::Link { rel: "stylesheet", href: TECHS_CSS } document::Stylesheet { href: asset!("/assets/styling/techs.css") }
div { class: "tech-cat", div { class: "tech-cat",
h3 { "{cat}" } h3 { "{cat}" }
div { class: "tech-row", div { class: "tech-row",
for tech in tech_vec { for tech in tech_vec {
TechCard { TechCard { tech_props: tech }
tech_props: tech,
}
} }
} }
} }

View File

@ -49,12 +49,10 @@ pub enum Route {
PageNotFound { route: Vec<String> }, PageNotFound { route: Vec<String> },
} }
const NOT_FOUND_CSS: Asset = asset!("/assets/styling/notFound.css");
#[component] #[component]
fn PageNotFound(route: Vec<String>) -> Element { fn PageNotFound(route: Vec<String>) -> Element {
rsx! { rsx! {
document::Link { rel: "stylesheet", href: NOT_FOUND_CSS } document::Stylesheet { href: asset!("/assets/styling/notFound.css") }
div { id: "not-found", div { id: "not-found",
h1 { "Page not found" } h1 { "Page not found" }
p { "We are terribly sorry, but the page you requested doesn't exist." } p { "We are terribly sorry, but the page you requested doesn't exist." }
@ -64,3 +62,15 @@ fn PageNotFound(route: Vec<String>) -> Element {
} }
} }
} }
// The server function at the endpoint "static_routes" will be called by the CLI to generate the list of static
// routes. You must explicitly set the endpoint to `"static_routes"` in the server function attribute instead of
// the default randomly generated endpoint.
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
// async fn static_routes() -> Result<Vec<String>, ServerFnError> {
// // The `Routable` trait has a `static_routes` method that returns all static routes in the enum
// Ok(Route::static_routes()
// .iter()
// .map(ToString::to_string)
// .collect())
// }

View File

@ -7,7 +7,7 @@ use personal_site::Route;
// The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles. // The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles.
const FAVICON: Asset = asset!("/assets/favicon.ico"); const FAVICON: Asset = asset!("/assets/favicon.ico");
// The asset macro also minifies some assets like CSS and JS to make bundled smaller // The asset macro also minifies some assets like CSS and JS to make bundled smaller
const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); // const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
fn main() { fn main() {
// The `launch` function is the main entry point for a dioxus app. It takes a component and renders it with the platform feature // The `launch` function is the main entry point for a dioxus app. It takes a component and renders it with the platform feature
@ -26,10 +26,34 @@ fn App() -> Element {
// In addition to element and text (which we will see later), rsx can contain other components. In this case, // In addition to element and text (which we will see later), rsx can contain other components. In this case,
// we are using the `document::Link` component to add a link to our favicon and main CSS file into the head of our app. // we are using the `document::Link` component to add a link to our favicon and main CSS file into the head of our app.
document::Link { rel: "icon", href: FAVICON } document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS } // document::Link { rel: "stylesheet", href: MAIN_CSS }
document::Stylesheet { href: asset!("/assets/styling/main.css") }
// The router component renders the route enum we defined above. It will handle synchronization of the URL and render // The router component renders the route enum we defined above. It will handle synchronization of the URL and render
// the layouts and components for the active route. // the layouts and components for the active route.
Router::<Route> {} Router::<Route> {}
} }
} }
#[cfg(feature = "ssg")]
fn main() {
use dioxus_fullstack::{incremental::IncrementalRendererConfig, prelude::*};
LaunchBuilder::new()
.with_cfg(
ServeConfig::builder()
.incremental(
IncrementalRendererConfig::new()
.static_dir(
std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.join("public"),
)
.clear_cache(false),
)
.enable_out_of_order_streaming(),
)
.launch(app);
}

View File

@ -1,11 +1,9 @@
use std::thread::Scope;
use crate::Route; use crate::Route;
use dioxus::{html::script::r#async, logger::tracing, prelude::*}; use dioxus::{logger::tracing, prelude::*};
use reqwest; use reqwest;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const BLOG_CSS: Asset = asset!("/assets/styling/blog.css"); // const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
/// The Blog page component that will be rendered when the current route is `[Route::Blog]` /// The Blog page component that will be rendered when the current route is `[Route::Blog]`
/// ///
@ -13,17 +11,32 @@ const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
/// re-run and the rendered HTML will be updated. /// re-run and the rendered HTML will be updated.
#[component] #[component]
pub fn Blog(blog_title: String) -> Element { pub fn Blog(blog_title: String) -> Element {
let blog_content = use_signal(|| { let blog_content = use_signal(move || {
"<h1>This is a blog #blog_title</h1> "<h1>This is a blog #blog_title</h1><p>
<p>
In blog #{blog_title}, we show how the Dioxus router works and In blog #{blog_title}, we show how the Dioxus router works and
how URL parameters can be passed as props to our route components. how URL parameters can be passed as props to our route components.
</p>" </p>"
.to_string() .to_string()
}); });
rsx! { // let blog_content = use_signal(move || BlogContent {
document::Link { rel: "stylesheet", href: BLOG_CSS } // blog_file_name: "blog_title".to_string(),
// blog_title: "This is a blog #blog_title".to_string(),
// date_last_edit: "2025-5-20".to_string(),
// tags: "#test".to_string(),
// html_blog_content: "<p>
// In blog #{blog_title}, we show how the Dioxus router works and
// how URL parameters can be passed as props to our route components.
// </p>"
// .to_string(),
// });
let blog = blog_content();
// let last_edit = &blog.date_last_edit;
// let tag = &blog.tags;
rsx! {
document::Stylesheet { href: asset!("/assets/styling/blog.css") }
document::Title { "Brock Tomlinson - {blog_title}" }
div { id: "blog", div { id: "blog",
// Content // Content
@ -44,6 +57,13 @@ pub fn Blog(blog_title: String) -> Element {
// } // }
// span { " <---> " } // span { " <---> " }
Link { to: Route::Blogs { page_num: 0 }, "Go Back" } Link { to: Route::Blogs { page_num: 0 }, "Go Back" }
// div { dangerous_inner_html: *&blog.html_blog_content.as_str(),
// h1 { "{blog_title}" }
// div {
// p { "{&blog.tags}" }
// p { "{&blog.date_last_edit}" }
// }
// }
div { dangerous_inner_html: blog_content } div { dangerous_inner_html: blog_content }
} }
} }
@ -63,20 +83,26 @@ async fn get_blog(blog_name: String) {
pub fn Blogs(page_num: u32) -> Element { pub fn Blogs(page_num: u32) -> Element {
let mut _num_limit: Signal<u8> = use_signal(|| 10); let mut _num_limit: Signal<u8> = use_signal(|| 10);
let blogs_resource: Resource<Vec<BlogPreview>> = let blogs_resource: Resource<Vec<BlogContent>> = use_resource(move || async move {
use_resource(move || async move { get_blogs_preview(_num_limit(), page_num).await }); get_blogs_preview(_num_limit(), page_num)
.await
.unwrap_or_else(|_| vec![])
});
rsx! { rsx! {
document::Link { rel: "stylesheet", href: BLOG_CSS } document::Stylesheet { href: asset!("/assets/styling/blog.css") }
div { id: "blogs", div { id: "blogs",
title { "Blogs" } document::Title { "Brock Tomlinson - Blogs" }
h1 { "Blogs" } div { id: "blogs-title",
p { h1 { "Blogs" }
"This is a collection of blog posts, ranging from tutorials, technologies I found interesting, and opinion pieces" p {
} "This is a collection of blog posts, ranging from tutorials, technologies I found interesting, and opinion pieces"
Link { to: Route::Home {}, }
button { "Home" } p { "These blogs are my opinion and mine alone" }
} }
// Link { to: Route::Home {},
// button { "Home" }
// }
// Link { // Link {
// to: Route::Blog { // to: Route::Blog {
// blog_title: "Test_Blog".to_string(), // blog_title: "Test_Blog".to_string(),
@ -85,61 +111,100 @@ pub fn Blogs(page_num: u32) -> Element {
// } // }
div { div {
if let Some(blogs) = &*blogs_resource.read() { if let Some(blogs) = &*blogs_resource.read() {
for blog in blogs.iter() { if blogs.len() > 0 {
for blog in blogs.iter() {
Link { Link {
to: Route::Blog { to: Route::Blog {
blog_title: blog.blog_file_name.clone(), blog_title: blog.blog_file_name.clone(),
}, },
div { dangerous_inner_html: blog.html_preview.as_str() } div { dangerous_inner_html: blog.html_blog_content.as_str(),
h1 { "{blog.blog_title}" }
div {
p { "{blog.tags}" }
p { "{blog.date_last_edit}" }
}
}
}
}
} else {
div { id: "blog-out-of",
p { "No more blogs available" }
Link { to: Route::Blogs { page_num: 0 },
button { "Go Back" }
}
} }
} }
} else { } else {
div { "Loading blogs..." } div { id: "blog-loading",
p { "Loading blogs..." }
}
} }
} }
div { div { id: "blog-nav",
Link {
to: Route::Blogs {
page_num: page_num + 1,
},
"Next"
}
if page_num > 0 { if page_num > 0 {
Link { Link {
to: Route::Blogs { to: Route::Blogs {
page_num: page_num - 1, page_num: page_num - 1,
}, },
"Go Back" button { "<-- Go Back" }
} }
} }
div {
label { "display: " }
select {
onchange: move |event| {
tracing::info!("Change happened {:?}", event.value());
_num_limit.set(event.value().parse::<u8>().unwrap_or(10));
},
option { "10" }
option { "25" }
option { "50" }
option { "100" }
}
}
Link {
to: Route::Blogs {
page_num: page_num + 1,
},
button { "Next -->" }
}
} }
} }
} }
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct BlogPreview { struct BlogContent {
pub blog_file_name: String, pub blog_file_name: String,
pub date_last_edit: String, pub date_last_edit: String,
pub html_preview: String, pub blog_title: String,
pub tags: String,
pub html_blog_content: String,
} }
async fn get_blogs_preview(_num_limit: u8, page_num: u32) -> Vec<BlogPreview> { async fn get_blogs_preview(
let res = reqwest::get(format!( _num_limit: u8,
"http://localhost:8000/blogs/{}/{}", page_num: u32,
_num_limit, page_num ) -> Result<Vec<BlogContent>, reqwest::Error> {
)) let client = reqwest::Client::new();
.await
.unwrap() let res: String = client
.text() .get(format!(
.await "http://localhost:8000/blogs/{}/{}",
.unwrap_or("".to_string()); _num_limit, page_num
))
.timeout(std::time::Duration::from_secs(10))
.send()
.await?
.text()
.await?;
let json: serde_json::Value = serde_json::from_str(&res).unwrap(); let json: serde_json::Value = serde_json::from_str(&res).unwrap();
// Extract the "Blogs" array and deserialize it into Vec<BlogPreview> // Extract the "Blogs" array and deserialize it into Vec<BlogContent>
let blogs: Vec<BlogPreview> = serde_json::from_value( let blogs: Vec<BlogContent> = serde_json::from_value(
json.get("Blogs") json.get("Blogs")
.cloned() .cloned()
.unwrap_or(serde_json::Value::Null), .unwrap_or(serde_json::Value::Null),
@ -147,5 +212,5 @@ async fn get_blogs_preview(_num_limit: u8, page_num: u32) -> Vec<BlogPreview> {
.unwrap_or_default(); .unwrap_or_default();
// tracing::info!("{:?}", blogs); // tracing::info!("{:?}", blogs);
blogs Ok(blogs)
} }

View File

@ -1,19 +1,18 @@
use dioxus::prelude::*; use dioxus::prelude::*;
const PROFESSIONAL_PHOTO_JPG: Asset = asset!("assets/professional_photo_2023.jpg"); const PROFESSIONAL_PHOTO_JPG: Asset = asset!("assets/professional_photo_2023.jpg");
const CONTACT_CSS: Asset = asset!("/assets/styling/contact.css");
#[component] #[component]
pub fn Contact() -> Element { pub fn Contact() -> Element {
rsx! { rsx! {
document::Link { href: CONTACT_CSS, rel: "stylesheet" } document::Stylesheet { href: asset!("/assets/styling/contact.css") }
h2 { "Contact" } h2 { "Contact" }
div { id: "contact", div { id: "contact",
div { div {
div { div {
img { img {
src: PROFESSIONAL_PHOTO_JPG, src: PROFESSIONAL_PHOTO_JPG,
alt: "Borck's professional photo", alt: "Brock's professional photo",
} }
} }
} }

View File

@ -6,8 +6,6 @@ use dioxus::prelude::*;
use crate::views::Contact; use crate::views::Contact;
const CONTACTME_CSS: Asset = asset!("/assets/styling/contactme.css");
#[component] #[component]
pub fn ContactMe() -> Element { pub fn ContactMe() -> Element {
let mut contact_me_name = use_signal(|| String::new()); let mut contact_me_name = use_signal(|| String::new());
@ -17,8 +15,8 @@ pub fn ContactMe() -> Element {
let mut _error_box_message = use_signal(|| String::new()); let mut _error_box_message = use_signal(|| String::new());
rsx! { rsx! {
document::Link { rel: "stylesheet", href: CONTACTME_CSS } document::Stylesheet { href: asset!("/assets/styling/contactme.css") }
title { "Brock Tomlinson - Contact" } document::Title { "Brock Tomlinson - Contact" }
div { id: "ContactMe", div { id: "ContactMe",
div { div {
h2 { "Get in Touch" } h2 { "Get in Touch" }

View File

@ -3,8 +3,6 @@ use crate::views::{Contact, Projects};
use crate::Route; use crate::Route;
use dioxus::prelude::*; use dioxus::prelude::*;
const HOME_CSS: Asset = asset!("/assets/styling/home.css");
#[component] #[component]
pub fn Home() -> Element { pub fn Home() -> Element {
let languages = vec![ let languages = vec![
@ -32,8 +30,8 @@ pub fn Home() -> Element {
]; ];
let platforms = vec!["AWS", "Cloudflare", "Vercel", "Netlify", "Gitea", "Github"]; let platforms = vec!["AWS", "Cloudflare", "Vercel", "Netlify", "Gitea", "Github"];
rsx!( rsx!(
document::Link { rel: "stylesheet", href: HOME_CSS } document::Title { "Brock Tomlinson - Home" }
title { "Brock Tomlinson" } document::Stylesheet { href: asset!("/assets/styling/home.css") }
div { div {
div { id: "home-intro", div { id: "home-intro",
h1 { "Hi I'm Brock" } h1 { "Hi I'm Brock" }

View File

@ -2,9 +2,6 @@ use crate::components::Ender;
use crate::Route; use crate::Route;
use dioxus::prelude::*; use dioxus::prelude::*;
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
const STD_COLOUR_AND_FONTS_CSS: Asset = asset!("assets/styling/standardColoursandFonts.css");
/// The Navbar component that will be rendered on all pages of our app since every page is under the layout. /// The Navbar component that will be rendered on all pages of our app since every page is under the layout.
/// ///
/// ///
@ -13,8 +10,8 @@ const STD_COLOUR_AND_FONTS_CSS: Asset = asset!("assets/styling/standardColoursan
#[component] #[component]
pub fn Navbar() -> Element { pub fn Navbar() -> Element {
rsx! { rsx! {
document::Link { rel: "stylesheet", href: NAVBAR_CSS } document::Stylesheet { href: asset!("/assets/styling/navbar.css") }
document::Link { rel: "stylesheet", href: STD_COLOUR_AND_FONTS_CSS } document::Stylesheet { href: asset!("assets/styling/standardColoursandFonts.css") }
div { id: "navbar", div { id: "navbar",
Link { to: Route::Home {}, "Home" } Link { to: Route::Home {}, "Home" }

View File

@ -5,7 +5,7 @@ use dioxus::prelude::*;
pub fn Projects(#[props(default = true)] independent_page: bool) -> Element { pub fn Projects(#[props(default = true)] independent_page: bool) -> Element {
rsx! { rsx! {
if independent_page { if independent_page {
title { "Brock Tomlinson - Projects" } document::Title { "Brock Tomlinson - Projects" }
} }
div { div {
h2 { "Projects" } h2 { "Projects" }
@ -74,8 +74,6 @@ pub fn Projects(#[props(default = true)] independent_page: bool) -> Element {
} }
} }
const PROJECT_CARDS_CSS: Asset = asset!("/assets/styling/projectCards.css");
#[component] #[component]
pub fn ProjectCards( pub fn ProjectCards(
website_link: Option<&'static str>, website_link: Option<&'static str>,
@ -87,7 +85,7 @@ pub fn ProjectCards(
#[props(default = "https://picsum.photos/200")] project_img: &'static str, #[props(default = "https://picsum.photos/200")] project_img: &'static str,
) -> Element { ) -> Element {
rsx! { rsx! {
document::Link { href: PROJECT_CARDS_CSS, rel: "stylesheet" } document::Stylesheet { href: asset!("/assets/styling/projectCards.css") }
div { class: "project-card", div { class: "project-card",
img { img {
src: "{project_img}", src: "{project_img}",