Compare commits

...

8 Commits

16 changed files with 375 additions and 137 deletions

2
Cargo.lock generated
View File

@ -3179,7 +3179,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "personal_site" name = "personal_site"
version = "1.6.0" version = "2.0.2"
dependencies = [ dependencies = [
"dioxus", "dioxus",
"reqwest", "reqwest",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "personal_site" name = "personal_site"
version = "1.6.0" version = "2.0.2"
authors = ["darkicewolf50 <brock.tomlinson@ucalgary.ca>"] authors = ["darkicewolf50 <brock.tomlinson@ucalgary.ca>"]
edition = "2021" edition = "2021"

2
assets/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /blogs/

View File

@ -1,5 +1,60 @@
#blog { #blog {
margin-top: 50px; margin-top: 2svh;
min-height: 80svh;
}
#blog_info {
display: flex;
flex-direction: column;
}
#blog_info ul {
display: flex;
flex-direction: row;
list-style-type: none;
margin: 0px;
padding: 0px;
}
#blog_info p {
padding: 2svh 0svw;
margin: 0px;
}
#blog_info h1 {
padding: 1svh 1svw;
margin: 2svh 0svw;
margin-bottom: 0px;
font-size: 2em;
}
#blog_info div {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 1svh 0svw;
border-bottom: var(--underlineTitle);
border-radius: var(--underlineTitleBorderRadius);
}
#blog_info div p {
padding: 0px;
margin: 0px;
}
#blog_info div div {
margin: 0px;
padding: 0px;
border: none;
}
#blog_info div p:last-child {
margin-right: 10svh;
}
#blog_info div ul li {
background-color: rgba(128, 0, 128, 0.2);
border-radius: 1rem;
padding: 0.25svh 8px;
margin: 0svh 0.25svh;
text-align: center;
} }
#blog a { #blog a {
@ -11,6 +66,28 @@
color: #91a4d2; color: #91a4d2;
} }
#blog_content {
padding: 2svh 0svw;
}
#blog_content h1,
h2,
h3 {
margin: 0px;
padding: 1svh 0svw;
border-bottom: none;
}
#blog_content p {
padding: 1svh 0svw;
margin: 0px;
text-wrap: wrap;
max-width: 25rem;
}
#blog #blog_content p {
max-width: max-content;
}
#blogs { #blogs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -50,6 +127,38 @@
color: #91a4d2; color: #91a4d2;
} }
.blog-preview {
display: flex;
flex-direction: column;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
padding: 0svh 2svw;
}
#blogs .blog-preview h1 {
border-bottom: none;
padding: 0svh 1svw;
margin: 0px;
}
#blogs .blog-preview button {
width: max-content;
border: 2px solid rgba(145, 164, 210, 0.4);
border-radius: var(--card-border-radius);
font-size: medium;
margin-top: auto;
margin-bottom: 4svh;
}
#blogs .blog-preview #blog_info div p:last-child {
margin-right: 0svh;
margin-left: 2svw;
}
#blogs .blog-preview #blog_info div div p:last-child {
margin: 0svh 0.25svh;
padding: 0.25svh 8px;
}
#blogs-title { #blogs-title {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -61,6 +170,15 @@
padding: 1svh 0svw; padding: 1svh 0svw;
} }
#blogs-on-show {
display: flex;
flex-flow: row wrap;
justify-content: center;
padding: 2svh 0svw;
row-gap: 2svh;
column-gap: 2svw;
}
#blog-loading { #blog-loading {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
/* flex-basis: 50%; */ /* flex-basis: 50%; */
} }

View File

@ -63,15 +63,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 4svh; margin-bottom: 4svh;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
} }
#experience div { #experience div {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
background-color: var(--card-background-color);
border-radius: var(--card-border-radius);
margin: 4svh 0px; margin: 4svh 0px;
} }

View File

@ -67,6 +67,10 @@
filter: grayscale(100%) invert(100%) brightness(2.5); filter: grayscale(100%) invert(100%) brightness(2.5);
} }
.project-title-info #dockerhub img {
filter: grayscale(100%) invert(100%) brightness(2.5);
}
.project-title-info img:hover { .project-title-info img:hover {
filter: brightness(0) saturate(100%) invert(65%) sepia(13%) saturate(733%) filter: brightness(0) saturate(100%) invert(65%) sepia(13%) saturate(733%)
hue-rotate(187deg) brightness(95%) contrast(90%); hue-rotate(187deg) brightness(95%) contrast(90%);
@ -77,6 +81,11 @@
hue-rotate(185deg) brightness(1.3) contrast(90%) saturate(100%); hue-rotate(185deg) brightness(1.3) contrast(90%) saturate(100%);
} }
.project-title-info #dockerhub img:hover {
filter: grayscale(100%) invert(165%) sepia(15%) saturate(733%)
hue-rotate(185deg) brightness(1.3) contrast(90%) saturate(100%);
}
.project-title-info div { .project-title-info div {
display: flex; display: flex;
gap: 1svw; gap: 1svw;

View File

@ -2,8 +2,8 @@
//! They can be used to defined common UI elements like buttons, forms, and modals. In this template, we define a Hero //! They can be used to defined common UI elements like buttons, forms, and modals. In this template, we define a Hero
//! component to be used in our app. //! component to be used in our app.
mod hero; // mod hero;
pub use hero::Hero; // pub use hero::Hero;
mod techs; mod techs;
pub use techs::TechCat; pub use techs::TechCat;

View File

@ -9,6 +9,15 @@ pub fn get_tech_logos_from_str(used_tech: &'static str) -> Element {
} }
} }
#[component]
pub fn set_meta_tags(description: &'static str, keywords: &'static str) -> Element {
rsx! {
document::Meta { name: "description", content: description }
document::Meta { name: "keywords", content: keywords }
document::Meta { name: "author", content: "Brock Tomlinson" }
}
}
#[derive(PartialEq, Props, Clone, Copy)] #[derive(PartialEq, Props, Clone, Copy)]
pub struct TechDes { pub struct TechDes {
pub tech_name: &'static str, pub tech_name: &'static str,

View File

@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
// use components::Hero; // use components::Hero;
use views::{Blog, Blogs, ContactMe, Home, Navbar, NewHome, Projects}; use views::{Blog, Blogs, ContactMe, Home, Navbar, Projects};
/// Define a components module that contains all shared components for our app. /// Define a components module that contains all shared components for our app.
mod components; mod components;
@ -42,8 +42,8 @@ pub enum Route {
#[route("/contact")] #[route("/contact")]
ContactMe {}, ContactMe {},
#[route("/new_home")] // #[route("/new_home")]
NewHome {}, // NewHome {},
// PageNotFound is a catch all route that will match any route and placing the matched segments in the route field // PageNotFound is a catch all route that will match any route and placing the matched segments in the route field
#[route("/:..route")] #[route("/:..route")]
PageNotFound { route: Vec<String> }, PageNotFound { route: Vec<String> },
@ -53,6 +53,7 @@ pub enum Route {
fn PageNotFound(route: Vec<String>) -> Element { fn PageNotFound(route: Vec<String>) -> Element {
rsx! { rsx! {
document::Stylesheet { href: asset!("/assets/styling/notFound.css") } document::Stylesheet { href: asset!("/assets/styling/notFound.css") }
document::Title { "Brock Tomlinson - Not Found" }
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." }
@ -62,15 +63,3 @@ 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

@ -13,6 +13,30 @@ 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
// you have enabled // you have enabled
dioxus::launch(App); dioxus::launch(App);
// for compiling the app
// dioxus::LaunchBuilder::new()
// // Set the server config only if we are building the server target
// .with_cfg(server_only! {
// ServeConfig::builder()
// // Enable incremental rendering
// .incremental(
// IncrementalRendererConfig::new()
// // Store static files in the public directory where other static assets like wasm are stored
// .static_dir(
// std::env::current_exe()
// .unwrap()
// .parent()
// .unwrap()
// .join("public")
// )
// // Don't clear the public folder on every build. The public folder has other files including the wasm
// // binary and static assets required for the app to run
// .clear_cache(false)
// )
// .enable_out_of_order_streaming()
// })
// .launch(App);
} }
/// App is the main component of our app. Components are the building blocks of dioxus apps. Each component is a function /// App is the main component of our app. Components are the building blocks of dioxus apps. Each component is a function
@ -35,25 +59,14 @@ fn App() -> Element {
} }
} }
#[cfg(feature = "ssg")] // The server function at the endpoint "static_routes" will be called by the CLI to generate the list of static
fn main() { // routes. You must explicitly set the endpoint to `"static_routes"` in the server function attribute instead of
use dioxus_fullstack::{incremental::IncrementalRendererConfig, prelude::*}; // the default randomly generated endpoint.
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
LaunchBuilder::new() // async fn static_routes() -> Result<Vec<String>, ServerFnError> {
.with_cfg( // // The `Routable` trait has a `static_routes` method that returns all static routes in the enum
ServeConfig::builder() // Ok(Route::static_routes()
.incremental( // .iter()
IncrementalRendererConfig::new() // .map(ToString::to_string)
.static_dir( // .collect())
std::env::current_exe() // }
.unwrap()
.parent()
.unwrap()
.join("public"),
)
.clear_cache(false),
)
.enable_out_of_order_streaming(),
)
.launch(app);
}

View File

@ -1,42 +1,37 @@
use crate::Route; use crate::Route;
use dioxus::{logger::tracing, prelude::*}; // use dioxus::logger::tracing::info;
use dioxus::prelude::*;
use reqwest; use reqwest;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// 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]`
/// ///
/// The component takes a `id` prop of type `i32` from the route enum. Whenever the id changes, the component function will be /// The component takes a `id` prop of type `i32` from the route enum. Whenever the id changes, the component function will be
/// 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(move || { let blog_resource = use_resource({
"<h1>This is a blog #blog_title</h1><p> let title = blog_title.clone();
In blog #{blog_title}, we show how the Dioxus router works and move || {
how URL parameters can be passed as props to our route components. let value = title.clone();
</p>" async move {
.to_string() get_blog(value).await.unwrap_or(BlogContent {
blog_file_name: String::new(),
date_last_edit: "9999-12-01".to_string(),
blog_title: "Not Found".to_string(),
tags: vec!["#error".to_string()],
html_blog_content: "<p>Blog not found</p>".to_string(),
})
}
}
}); });
// let blog_content = use_signal(move || BlogContent {
// 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! { rsx! {
document::Stylesheet { href: asset!("/assets/styling/blog.css") } document::Stylesheet { href: asset!("/assets/styling/blog.css") }
document::Title { "Brock Tomlinson - {blog_title}" } document::Title { "Brock Tomlinson - {blog_title.clone()}" }
// document::Meta { name: "author", content: "Brock Tomlinson" }
document::Meta { name: "robots", content: "noindex, nofollow" }
div { id: "blog", div { id: "blog",
// Content // Content
@ -57,42 +52,88 @@ 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(), if let Some(blog_content) = &*blog_resource.read() {
// h1 { "{blog_title}" } article {
// div { header { id: "blog_info",
// p { "{&blog.tags}" } h1 { "{blog_content.blog_title}" }
// p { "{&blog.date_last_edit}" } div {
// } ul {
// } for tag in &blog_content.tags {
div { dangerous_inner_html: blog_content } li { "{tag}" }
}
}
p { "{&blog_content.date_last_edit}" }
}
}
section {
id: "blog_content",
dangerous_inner_html: *&blog_content.html_blog_content.as_str(),
}
}
} else {
p { "Loading..." }
}
} }
} }
} }
async fn get_blog(blog_name: String) { async fn get_blog(blog_name: String) -> Result<BlogContent, reqwest::Error> {
let res = reqwest::get("https://www.rust-lang.org") let client = reqwest::Client::new();
.await
.unwrap() let res = client
.text() .get(format!("blogs/blog/{}", blog_name))
.await .timeout(std::time::Duration::from_secs(10))
.unwrap_or("".to_string()); .send()
tracing::info!("{}", res); .await?
.json::<BlogContent>()
// .text()
.await?;
// tracing::info!("{:?}", res);
Ok(res)
// to be removed
// let blog = serde_json::from_str(&res).unwrap_or(BlogContent {
// 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(),
// });
// Ok(blog)
// Ok(BlogContent {
// 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(),
// })
} }
#[component] #[component]
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<BlogContent>> = use_resource(move || async move { let blogs_resource: Resource<Vec<BlogContent>> =
get_blogs_preview(_num_limit(), page_num) use_resource(use_reactive!(|(_num_limit, page_num)| async move {
.await get_blogs_preview(_num_limit(), page_num)
.unwrap_or_else(|_| vec![]) .await
}); .unwrap_or_else(|_| vec![])
}));
rsx! { rsx! {
document::Stylesheet { href: asset!("/assets/styling/blog.css") } document::Stylesheet { href: asset!("/assets/styling/blog.css") }
document::Meta { name: "robots", content: "noindex, nofollow" }
document::Title { "Brock Tomlinson - Blogs" }
div { id: "blogs", div { id: "blogs",
document::Title { "Brock Tomlinson - Blogs" }
div { id: "blogs-title", div { id: "blogs-title",
h1 { "Blogs" } h1 { "Blogs" }
p { p {
@ -100,31 +141,32 @@ pub fn Blogs(page_num: u32) -> Element {
} }
p { "These blogs are my opinion and mine alone" } p { "These blogs are my opinion and mine alone" }
} }
// Link { to: Route::Home {}, div { id: "blogs-on-show",
// button { "Home" }
// }
// Link {
// to: Route::Blog {
// blog_title: "Test_Blog".to_string(),
// },
// button { "To Test Blog" }
// }
div {
if let Some(blogs) = &*blogs_resource.read() { if let Some(blogs) = &*blogs_resource.read() {
if blogs.len() > 0 { if blogs.len() > 0 {
for blog in blogs.iter() { for blog in blogs.iter() {
Link { Link {
class: "blog-preview",
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_blog_content.as_str(), div { id: "blog_info",
h1 { "{blog.blog_title}" } h1 { "{blog.blog_title}" }
div { div {
p { "{blog.tags}" } div {
p { "{blog.date_last_edit}" } for tag in &blog.tags {
p { "{tag}" }
}
}
p { "{&blog.date_last_edit}" }
} }
} }
div {
id: "blog_content",
dangerous_inner_html: *&blog.html_blog_content.as_str(),
}
button { "Read More Here" }
} }
} }
} else { } else {
@ -154,7 +196,6 @@ pub fn Blogs(page_num: u32) -> Element {
label { "display: " } label { "display: " }
select { select {
onchange: move |event| { onchange: move |event| {
tracing::info!("Change happened {:?}", event.value());
_num_limit.set(event.value().parse::<u8>().unwrap_or(10)); _num_limit.set(event.value().parse::<u8>().unwrap_or(10));
}, },
option { "10" } option { "10" }
@ -180,7 +221,7 @@ struct BlogContent {
pub blog_file_name: String, pub blog_file_name: String,
pub date_last_edit: String, pub date_last_edit: String,
pub blog_title: String, pub blog_title: String,
pub tags: String, pub tags: Vec<String>,
pub html_blog_content: String, pub html_blog_content: String,
} }
@ -190,27 +231,23 @@ async fn get_blogs_preview(
) -> Result<Vec<BlogContent>, reqwest::Error> { ) -> Result<Vec<BlogContent>, reqwest::Error> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res: String = client let res = client
.get(format!( .get(format!("blogs/{}/{}", _num_limit, page_num))
"http://localhost:8000/blogs/{}/{}",
_num_limit, page_num
))
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.send() .send()
.await? .await?
.text() .json::<Vec<BlogContent>>()
// .text()
.await?; .await?;
Ok(res)
// let json: serde_json::Value = serde_json::from_str(&res).unwrap();
// let blogs: Vec<BlogContent> = serde_json::from_value(json).unwrap_or_default();
let json: serde_json::Value = serde_json::from_str(&res).unwrap(); // // Extract the "Blogs" array and deserialize it into Vec<BlogContent>
// let blogs: Vec<BlogContent> = serde_json::from_value(
// Extract the "Blogs" array and deserialize it into Vec<BlogContent> // json.get("Blogs")
let blogs: Vec<BlogContent> = serde_json::from_value( // .cloned()
json.get("Blogs") // .unwrap_or(serde_json::Value::Null),
.cloned() // )
.unwrap_or(serde_json::Value::Null), // .unwrap_or_default();
)
.unwrap_or_default();
// tracing::info!("{:?}", blogs);
Ok(blogs)
} }

View File

@ -88,11 +88,7 @@ async fn send_message(name: String, email: String, message: String, mut recived:
}); });
let client = Client::new(); let client = Client::new();
let res = client let res = client.post("").json(&json_to_send).send().await;
.post("https://discord.com/api/webhooks/1371617469281861772/uARm18pvzzs4DVNLSYNYCyl7CQk_7eglqGmBabQASow2L7NHgGRHzQhkSAKaOIZmLnn1")
.json(&json_to_send)
.send()
.await;
match res { match res {
Ok(_) => { Ok(_) => {
recived.set("Sent Sucessfully, I will be in contact with you soon".to_string()); recived.set("Sent Sucessfully, I will be in contact with you soon".to_string());

View File

@ -8,8 +8,8 @@
//! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines //! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines
//! a common wrapper around all child routes. //! a common wrapper around all child routes.
mod new_home; // mod new_home;
pub use new_home::NewHome; // pub use new_home::NewHome;
mod blog; mod blog;
pub use blog::{Blog, Blogs}; pub use blog::{Blog, Blogs};

View File

@ -1,7 +1,11 @@
use crate::components::Ender; use crate::components::Ender;
use crate::helper_fun::set_meta_tags;
use crate::Route; use crate::Route;
use dioxus::prelude::*; use dioxus::prelude::*;
const _ROBOTS_TXT: Asset = asset!("/assets/robots.txt");
const PROFESSIONAL_PHOTO_JPG: Asset = asset!("assets/professional_photo_2023.jpg");
/// 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.
/// ///
/// ///
@ -9,9 +13,43 @@ use dioxus::prelude::*;
/// routes will be rendered under the outlet inside this component /// routes will be rendered under the outlet inside this component
#[component] #[component]
pub fn Navbar() -> Element { pub fn Navbar() -> Element {
let PHOTO_FORMAT_URL = format!("https://darkicewolf50.pages.dev{PROFESSIONAL_PHOTO_JPG}");
rsx! { rsx! {
document::Stylesheet { href: asset!("/assets/styling/navbar.css") } document::Stylesheet { href: asset!("/assets/styling/navbar.css") }
document::Stylesheet { href: asset!("assets/styling/standardColoursandFonts.css") } document::Stylesheet { href: asset!("assets/styling/standardColoursandFonts.css") }
set_meta_tags {
description: "Fourth year Software Engineering student specializing in full-stack development with a backend focus. Always improving through experience.",
keywords: "webdev Rust software engineer projects blog darkicewol50",
}
document::Meta { name: "robots", content: "index, follow" }
document::Link { rel: "canonical", href: "https://darkicewolf50.pages.dev/" }
document::Meta {
name: "google-site-verification",
content: "lsAs9c2Pv7c6Sm26z1hd2YqR2depbp4sJddIDYKHkxY",
}
document::Meta {
property: "og:title",
content: "Brock Tomlinson - Software Engineering Student",
}
document::Meta {
property: "og:description",
content: "Fourth year Software Engineering student specializing in full-stack development with a backend focus. Always improving through experience.",
}
document::Meta { property: "og:type", content: "website" }
document::Meta { property: "og:url", content: "https://darkicewolf50.pages.dev/" }
document::Meta { name: "twitter:card", content: "summary_large_image" }
document::Meta {
name: "twitter:title",
content: "Brock Tomlinson - Software Engineer",
}
document::Meta {
name: "twitter:description",
content: "Fourth year Software Engineering student specializing in full-stack development with a backend focus. Always improving through experience.",
}
document::Meta { name: "twitter:image", content: PHOTO_FORMAT_URL.clone() }
document::Meta { property: "og:image", content: PHOTO_FORMAT_URL.clone() }
div { id: "navbar", div { id: "navbar",
Link { to: Route::Home {}, "Home" } Link { to: Route::Home {}, "Home" }

View File

@ -13,21 +13,43 @@ pub fn Projects(#[props(default = true)] independent_page: bool) -> Element {
} }
div { class: "project-section", div { class: "project-section",
ProjectCards { ProjectCards {
project_name: "Portfolio Site Version 1.1.0", project_name: "Personal Backend",
gitea_link: "https://gitea.bajacloud.duckdns.org/darkicewolf50/darkicewolf50Cloud",
dockerhub_link: "https://hub.docker.com/r/darkicewolf50/darkicewolf50cloud",
project_img: "https://actix.rs/img/logo.png",
techs_used: vec![
"Rust",
"Actix",
"Github Actions",
"Docker",
"Traefik",
"Gitea",
"Git",
"Github",
],
project_des: "I find that this is a much better option in compareision to FastAPI as it does not require a post request
to input data instead it give the option for the url to give the parameters it needs. I don't have any complains about using Actix, its mature stable and fairly popular.
This backend application also uses comrak to convert markdown blogs into html docuemnts that are then seen by you the user.
This honestly was a fun challenge getting the blogs previews, ensuring correctness and not allowing for any unknown states,
this will serve as a great basis for any future backend requirements that I may have.",
}
ProjectCards {
project_name: "Portfolio Site Version 2.0.1",
website_link: "https://darkicewolf50.pages.dev", website_link: "https://darkicewolf50.pages.dev",
gitea_link: "https://gitea.bajacloud.duckdns.org/darkicewolf50/personal_site", gitea_link: "https://gitea.bajacloud.duckdns.org/darkicewolf50/personal_site",
project_img: "https://res.cloudinary.com/dpgrgsh7g/image/upload/v1745630861/Portfolio_site_k4mhmj.png", project_img: "https://res.cloudinary.com/dpgrgsh7g/image/upload/v1745630861/Portfolio_site_k4mhmj.png",
techs_used: vec!["Rust", "CSS", "Dioxus", "Git", "Gitea"], techs_used: vec!["Rust", "CSS", "Dioxus", "Git", "Gitea"],
project_des: "This project was a great test of my newly learned Rust. project_des: "This project was a great test of my newly learned Rust.
This minor update added functionality for the contact me, the ground work for the blogs part of the site, as well as many minor This major update added functionality for the contact me, the blogs part of the site, as well as many minor
UI consistencies to ensure that all of the buttons and links felt like buttons and links. UI consistencies to ensure that all of the buttons and links felt like buttons and links.
I was surprise how easy it was to set up a discord webhook using the 'reqwest' crate. I was surprise how easy it was to set up a discord webhook using the 'reqwest' crate.
As I continue on I find myself struggling with how and why to use databases for content I generate. As I continue on I find myself struggling with how and why to use databases for content I generate.
I think using tools like disocrd webhooks and email notifications are great for users but certainly not great for reading data from. I think using tools like disocrd webhooks and email notifications are great for users but certainly not great for reading data from.
The part I am both excited for and deading is going to be the blogs component which I believe I have solved for now.", I was very satisfied with serde, and comrak for converting markdown fiels into html.
I use this extensively for the blogs search menu and the blog itself to display the blog itself in a consistent way without needing to write a whole library.",
} }
ProjectCards { ProjectCards {
project_name: "Portfolio Site", project_name: "Portfolio Site 1.0.0",
website_link: "https://darkicewolf50.github.io", website_link: "https://darkicewolf50.github.io",
github_link: "https://github.com/darkicewolf50/darkicewolf50.github.io", github_link: "https://github.com/darkicewolf50/darkicewolf50.github.io",
project_img: "https://res.cloudinary.com/dpgrgsh7g/image/upload/v1745630861/Portfolio_site_k4mhmj.png", project_img: "https://res.cloudinary.com/dpgrgsh7g/image/upload/v1745630861/Portfolio_site_k4mhmj.png",
@ -40,6 +62,7 @@ pub fn Projects(#[props(default = true)] independent_page: bool) -> Element {
ProjectCards { ProjectCards {
project_name: "UCalgary Baja Backend", project_name: "UCalgary Baja Backend",
project_img: "https://www.svgrepo.com/show/448221/docker.svg", project_img: "https://www.svgrepo.com/show/448221/docker.svg",
dockerhub_link: "https://hub.docker.com/r/darkicewolf50/uofcbajacloud",
techs_used: vec!["Python", "FastAPI", "Github Actions", "Docker", "Traefik", "Git", "Github"], techs_used: vec!["Python", "FastAPI", "Github Actions", "Docker", "Traefik", "Git", "Github"],
project_des: "This is going to be extremely cost saving for the non-profit club UCalgary Baja. project_des: "This is going to be extremely cost saving for the non-profit club UCalgary Baja.
Using automated uploads and linting to check the Python and FastAPI code was excellent for learning how to self-host a web server. Using automated uploads and linting to check the Python and FastAPI code was excellent for learning how to self-host a web server.
@ -79,6 +102,7 @@ pub fn ProjectCards(
website_link: Option<&'static str>, website_link: Option<&'static str>,
github_link: Option<&'static str>, github_link: Option<&'static str>,
gitea_link: Option<&'static str>, gitea_link: Option<&'static str>,
dockerhub_link: Option<&'static str>,
project_name: &'static str, project_name: &'static str,
techs_used: Vec<&'static str>, techs_used: Vec<&'static str>,
project_des: &'static str, project_des: &'static str,
@ -104,6 +128,11 @@ pub fn ProjectCards(
get_tech_logos_from_str { used_tech: "Gitea" } get_tech_logos_from_str { used_tech: "Gitea" }
} }
} }
if let Some(dockerhub) = dockerhub_link {
a { href: "{dockerhub}", id: "dockerhub",
get_tech_logos_from_str { used_tech: "Docker" }
}
}
if let Some(site) = website_link { if let Some(site) = website_link {
a { href: "{site}", a { href: "{site}",
get_tech_logos_from_str { used_tech: "Internet" } get_tech_logos_from_str { used_tech: "Internet" }