//! # Rust Letter Frontend //! //! Rendering functions for the site using [Dioxus](https://dioxuslabs.com/). #![allow(non_snake_case)] /// A module that handles the functions needed /// to render the site. pub mod void_app { // import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types pub use dioxus::prelude::*; use markdown::{self, Options}; use rust_embed::RustEmbed; use std::collections::VecDeque; #[cfg(any(target_family = "wasm"))] use dioxus_helmet::Helmet; #[cfg(any(target_family = "wasm"))] use dioxus_router::{Link, Redirect, Route, Router}; #[derive(PartialEq, Props)] pub struct PoemRequest { pub slug: String, pub dark_mode: Option, } #[derive(PartialEq, Props)] pub struct DarkModeProps { pub dark_mode: bool, pub slug: Option, } #[derive(PartialEq, Props)] struct ButtonProps { title: String, slug: String, } #[derive(PartialEq, Props)] struct PoemData { title: Option, content: Option, creation_date: Option, slug: Option, dark_mode: Option, } #[derive(Props)] struct PoemChildren<'a> { children: Element<'a>, } #[derive(RustEmbed)] #[folder = "data/poems"] pub struct Poems; #[cfg(target_family = "wasm")] pub fn DioxusApp(cx: Scope) -> Element { use dioxus_router::Redirect; let poem_list = get_poem_list(); cx.render(rsx! { Router { Route { to: "/", HomePage {} } Route { to: "/poems/:slug/", PoemPage {} } Route { to: "", PageNotFound {} } } }) } #[cfg(target_family = "wasm")] fn PageNotFound(cx: Scope) -> Element { cx.render(rsx! { p { "That page doesn't exist, sorry!" } Redirect { to: "/" } }) } fn MutantStandardFooter(cx: Scope) -> Element { cx.render(rsx!{ div { class: "flex p-4 text-md text-center ring-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", "This site uses Mutant Standard emoji, which are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License" } }) } #[cfg(any(target_family = "windows", target_family = "unix"))] fn BackToHomePage(cx: Scope) -> Element { cx.render(rsx!{ a { class: "flex justify-center p-4 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", href: "/", p { "Back to the homepage" } } }) } #[cfg(target_family = "wasm")] fn BackToHomePage(cx: Scope) -> Element { cx.render(rsx!{ Link { to: "flex justify-center p-4 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", href: "/", p { "Back to the homepage" } } }) } #[cfg(any(target_family = "windows", target_family = "unix"))] fn NavigationButton(cx: Scope) -> Element { let title = cx.props.title.clone(); let title_ref = title.as_str(); let slug = cx.props.slug.clone(); let slug_ref = slug.as_str(); cx.render(rsx!{ a { class: "flex mx-auto max-w-full justify-center p-4 ml-2 mr-2 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", href: "{slug_ref}", "{title_ref}" } }) } #[cfg(target_family = "wasm")] fn NavigationButton(cx: Scope) -> Element { let title = cx.props.title.clone(); let title_ref = title.as_str(); let slug = cx.props.slug.clone(); let slug_ref = slug.as_str(); cx.render(rsx!{ Link { class: "flex mx-auto max-w-full justify-center p-4 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", to: slug_ref, title_ref } }) } #[cfg(any(target_family = "windows", target_family = "unix"))] fn DarkModeButton(cx: Scope) -> Element { let slug = cx .props .slug .clone() .expect("Slug found to know where to redirect after dark mode setting."); let slug_ref = slug.as_str(); let dark_mode = cx.props.dark_mode; cx.render(rsx! { a { href: "/?dark_mode&callback={slug_ref}", match dark_mode { true => { rsx! { img { src: "/images/white_square_button.png", alt: "A white square button that can toggle dark mode.", } } }, false => { rsx! { img { src: "/images/black_square_button.png", alt: "A black square button that can toggle dark mode.", } } }, } } }) } #[cfg(target_family = "wasm")] fn DarkModeButton(cx: Scope) -> Element { let slug = cx .props .slug .clone() .expect("Slug found to know where to redirect after dark mode setting."); let slug_ref = slug.as_str(); let dark_mode = cx.props.dark_mode; cx.render(rsx! { Link { to: "{slug_ref}/toggle-dark-mode", match dark_mode { true => { rsx! { img { src: "/images/white_square_button.png", alt: "A white square button that can toggle dark mode.", } } }, false => { rsx! { img { src: "/images/black_square_button.png", alt: "A black square button that can toggle dark mode.", } } }, } } }) } #[cfg(any(target_family = "unix", target_family = "windows"))] fn PoemButton(cx: Scope) -> Element { let title = cx.props.title.clone(); let title_ref = title.as_str(); let slug = cx.props.slug.clone(); let slug_ref = slug.as_str(); cx.render(rsx!{ a { href: "/poems/{slug_ref}", li { class: "p-4 ml-2 mr-2 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", "{title_ref}" } } }) } #[cfg(target_family = "wasm")] fn PoemButton(cx: Scope) -> Element { let title = cx.props.title.clone(); let title_ref = title.as_str(); let slug = cx.props.slug.clone(); let slug_ref = slug.as_str(); cx.render(rsx!{ Link { to: "/poems/{slug_ref}", li { class: "p-4 ml-2 mr-2 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", dangerous_inner_html: "{title_ref}" } } }) } /// Renders the app and returns the rendered Element. pub fn HomePage(cx: Scope) -> Element { let slug = cx.props.slug.clone().expect("Slug for dark mode redirect."); // let slug_ref = slug.as_str(); let dark_mode = cx.props.dark_mode.clone(); cx.render(rsx!{ div { class: "min-h-screen font-nerd bg-alice-werefox-grey-light dark:bg-alice-werefox-grey", div { class: "container space-y-4 mx-auto p-4", div { class: "p-4 ring-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", p { class: "flex flex-row mx-auto max-w-full justify-center text-xl text-center", "A Letter to the Void " DarkModeButton { slug: slug, dark_mode: dark_mode } } } div { class: "flex p-4 ml-2 mr-2 ring-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", p { class: "text-lg text-center", "Welcome, and I hope you enjoy your stay!" br {} "\"A Letter to the Void\" is a passion project of mine in which I wrote poems about my past life experiences, present, and hopes for the future throughout my transition." br {} "The topics range from my feelings through transitioning (of course), past abuse, mental health exploration, and an overall journey to grow and become a better creature." br {} "I hope you enjoy the time you spend here, and sincerely, thank you." br {} br {} "🖤 Alice Icehart Werefox" } } NavigationButton { title: "See Latest Entry".to_string(), slug: "/poems/latest".to_string() } NavigationButton { title: "See Oldest Entry".to_string(), slug: "/poems/oldest".to_string() } NavigationButton { title: "See All Entries".to_string(), slug: "/poems".to_string() } MutantStandardFooter {} } } }) } fn get_poem_list() -> Vec<(String, String)> { let mut poem_list = Vec::new(); for p in Poems::iter() { let filename = p.to_string(); let poem_content = Poems::get(&filename).expect("Found poem {filename:?}"); let mut poem_to_str = std::str::from_utf8(poem_content.data.as_ref()) .expect("Title is valid UT8.") .lines(); let title_markdown = poem_to_str.next().expect("No title specified."); let title = markdown::to_html_with_options(title_markdown, &Options::gfm()).unwrap(); let slug = String::from(filename.trim_end_matches(".md")); poem_list.push((title.clone(), slug.clone())); } log::trace!("{poem_list:?}"); poem_list } pub fn get_oldest_entry() -> String { let mut poem_list = VecDeque::from(get_poem_list()); poem_list .pop_front() .expect("There is an entry in this list of poems.") .1 } pub fn get_latest_entry() -> String { let mut poem_list = get_poem_list(); poem_list .pop() .expect("There is an entry in this list of poems.") .1 } pub fn get_previous_entry(slug: String) -> Option { let poem_list = get_poem_list(); poem_list.iter().enumerate().find_map(|(index, p)| { if p.1 == slug { if index != 0 { Some(poem_list[index - 1].1.clone()) } else { None } } else { None } }) } pub fn get_next_entry(slug: String) -> Option { let poem_list = get_poem_list(); poem_list.iter().enumerate().find_map(|(index, p)| { if p.1 == slug { if index != poem_list.len() - 1 { Some(poem_list[index + 1].1.clone()) } else { None } } else { None } }) } /// Renders the app and returns the rendered Element. pub fn PoemListPage(cx: Scope) -> Element { let slug = cx.props.slug.clone().expect("Slug for dark mode redirect."); let dark_mode = cx.props.dark_mode.clone(); cx.render(rsx!{ div { class: "min-h-screen font-nerd bg-alice-werefox-grey-light dark:bg-alice-werefox-grey", div { class: "container space-y-4 mx-auto p-4", div { class: "p-4 ring-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", p { class: "flex flex-row mx-auto max-w-full justify-center text-lg text-center", "A Letter to the Void " DarkModeButton { slug: slug, dark_mode: dark_mode } } } BackToHomePage {} PoemList {} BackToHomePage {} MutantStandardFooter {} } } }) } fn PoemList(cx: Scope) -> Element { let poem_list = get_poem_list(); cx.render(rsx! { ul { class: "flex flex-col space-y-4", poem_list.into_iter().map(|p| { rsx!{ PoemButton { title: p.0, slug: p.1 } } }) } }) } #[cfg(any(target_family = "unix", target_family = "windows"))] pub fn PoemPage(cx: Scope) -> Element { let slug = cx.props.slug.clone(); let mut oldest_uri = "#".to_string(); if get_oldest_entry() != slug.to_string() { oldest_uri = format!("/poems/oldest"); } let mut previous_uri = "#".to_string(); if let Some(_) = get_previous_entry(slug.clone()) { previous_uri = format!("/poems/previous/?current={slug}"); } let mut next_uri = "#".to_string(); if let Some(_) = get_next_entry(slug.clone()) { next_uri = format!("/poems/next/?current={slug}"); } let mut latest_uri = "#".to_string(); if get_latest_entry() != slug.to_string() { latest_uri = format!("/poems/latest"); } let dark_mode = cx .props .dark_mode .clone() .expect("Dark mode prop not passed."); cx.render(rsx!{ div { class: "min-h-screen font-nerd bg-alice-werefox-grey-light dark:bg-alice-werefox-grey", div { class: "container space-y-4 mx-auto p-4", BackToHomePage {} GetPoem { slug: slug, dark_mode: dark_mode } div { class: "grid md:grid-cols-4 md:grid-rows-1 grid-cols-1 grid-rows-4 gap-y-4", NavigationButton { title: "Oldest".to_string(), slug: oldest_uri } NavigationButton { title: "Previous".to_string(), slug: previous_uri } NavigationButton { title: "Next".to_string(), slug: next_uri } NavigationButton { title: "Latest".to_string(), slug: latest_uri } } BackToHomePage {} MutantStandardFooter {} } } }) } #[cfg(target_family = "wasm")] fn PoemPage(cx: Scope) -> Element { let slug = String::from( dioxus_router::use_route(cx) .segment("slug") .expect("No slug specified."), ); let dark_mode = cx .props .dark_mode .clone() .expect("Dark mode prop not passed."); cx.render(rsx!{ div { class: "min-h-screen font-nerd bg-alice-werefox-grey-light dark:bg-alice-werefox-grey", div { class: "container space-y-4 mx-auto p-4", BackToHomePage {} GetPoem { slug: slug, dark_mode: dark_mode } BackToHomePage {} MutantStandardFooter {} } } }) } // fn PoemButton(cx: Scope) -> Element { // let title = cx.props.title.clone().expect("No title specified."); // let slug = cx.props.slug.clone().expect("No slug specified."); // let class = String::from("p-4 ml-2 mr-2 text-xl text-center ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition"); // cx.render(rsx!{ // PoemButtonLink { title: title, slug: slug, classes: class } // }) // } // fn PoemButton(cx: Scope) -> Element { // let title = cx.props.title.clone().expect("No title specified."); // let slug = cx.props.slug.clone().expect("No slug specified."); // let slug_ref = slug.as_str(); // cx.render(rsx!{ // Link { to: "/poems/{slug_ref}", // li { class: "p-4 ml-2 mr-2 ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", // div { class: "mx-auto max-w-fit flex justify-center text-lg text-center", // dangerous_inner_html: "{title}" // } // } // } // }) // } fn GetPoem(cx: Scope) -> Element { // It would be good to implement some kind of "get_poem_data" or "into" functionality for the struct, so I don't have to write all this here. let dark_mode = cx .props .dark_mode .clone() .expect("Dark mode prop not passed."); let slug = String::from(cx.props.slug.clone().expect("No slug specified.")); let filename = String::from(String::from(slug.clone()) + ".md"); let creation_date = String::from(String::from("
Written on: ") + filename.split("_").next().unwrap()); let poem_content = Poems::get(&filename).expect("Found poem {filename:?}"); let mut poem_to_str = std::str::from_utf8(poem_content.data.as_ref()) .expect("Title is valid UT8.") .lines(); let poem_title = poem_to_str.next().unwrap(); let poem_content = poem_to_str.into_iter().collect::>().join("\n"); let poem_title_to_html_string = markdown::to_html_with_options(poem_title, &Options::gfm()).unwrap(); let poem_content_to_html_string = markdown::to_html_with_options(poem_content.as_str(), &Options::gfm()).unwrap(); cx.render(rsx! { MakePoem{ // data: PoemData{ // title: None, // content: Some(poem_content_to_html_string.clone()), // creation_date: Some(creation_date.clone()), // slug: Some(slug.clone()), // dark_mode: Some(dark_mode), // }, RenderPoemTitle { title: poem_title_to_html_string, slug: slug, dark_mode: dark_mode } RenderPoemElement { content: poem_content_to_html_string, creation_date: creation_date } }}) } // #[cfg(any(target_family = "unix", target_family = "windows"))] fn RenderPoemTitle(cx: Scope) -> Element { let slug = cx.props.slug.clone().expect("Slug prop was not passed."); let callback = format!("/poems/{slug}"); let dark_mode = cx .props .dark_mode .clone() .expect("Dark mode prop not passed."); let title = cx.props.title.clone().expect("No title specified."); cx.render(rsx!{ span { class: "p-4 ml-2 mr-2 flex text-xl text-center ring-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", span { class: "flex flex-row align-middle mx-auto max-w-full justify-center", "{title} " DarkModeButton { slug: callback, dark_mode: dark_mode } } } }) } // #[cfg(target_family = "wasm")] // fn RenderPoemTitle(cx: Scope) -> Element { // let slug = cx.props.slug.clone().expect("Slug prop was not passed."); // let dark_mode = cx.props.dark_mode.clone().expect("Dark mode prop not passed."); // let title = cx // .props // .title // .clone() // .expect("This poem has an empty title."); // cx.render(rsx!{ // p { class: "mx-auto max-w-fit flex flex-row justify-center bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light p-4", // dangerous_inner_html: "{title} ", // a { class: "", // href: "/poems/{slug}/toggle-dark-mode", // match dark_mode { // true => { // rsx! { // img { class: "", // src: "/images/white_square_button.png", // alt: "A white square button that can toggle dark mode.", // } // } // }, // false => { // rsx! { // img { class: "", // src: "/images/black_square_button.png", // alt: "A black square button that can toggle dark mode.", // } // } // }, // } // } // } // }) // } #[cfg(any(target_family = "unix", target_family = "windows"))] fn RenderPoemElement(cx: Scope) -> Element { let content = cx.props.content.clone().expect("No content specified."); let creation_date = cx .props .creation_date .clone() .expect("No creation date specified."); cx.render(rsx! { div { class: "flex p-2 mx-auto max-w-full justify-center", details { class: "group p-4 max-w-fit space-y-4 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-4 ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light", summary { class: "group-open:before:content-['Close'] before:content-['Open'] flex justify-center p-2 ring-2 bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark ring-alice-werefox-red-dark dark:ring-alice-werefox-red text-alice-werefox-grey-dark dark:text-alice-werefox-grey-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:ring-alice-werefox-blue dark:hover:ring-alice-werefox-blue hover:animate-yip transition", } div { class: "font-nerd flex flex-col space-y-4 py-4", "{content}{creation_date}" } } } }) } #[cfg(target_family = "wasm")] fn RenderPoemElement(cx: Scope) -> Element { let content = cx.props.content.clone().expect("No content specified."); let creation_date = cx .props .creation_date .clone() .expect("No creation date specified."); cx.render(rsx! { div { class: "font-nerd flex flex-col space-y-4 mx-4 py-4", dangerous_inner_html: "{content}{creation_date}" } }) } fn MakePoem<'a>(cx: Scope<'a, PoemChildren<'a>>) -> Element { cx.render(rsx! { div { class: "flex-col space-y-4", &cx.props.children } }) } }