//! # Rust Info Site //! //! Rendering functions for the site using [Dioxus](https://dioxuslabs.com/). #![allow(non_snake_case)] pub mod components; pub mod utils; // Urls are relative to your Cargo.toml file const _TAILWIND_URL: &str = manganis::mg!(file("public/tailwind.css")); /// A module that handles the functions needed /// to render the site. pub mod info_app { // import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types pub use dioxus::prelude::*; use serde::{Deserialize, Serialize}; use crate::components::basic_page::*; use crate::components::faq_card::*; use crate::components::home_page::*; use crate::components::identity_button::*; use crate::components::introduction_card::*; use crate::components::page_button::*; use crate::components::project_card::*; use crate::components::project_foldable::*; use crate::components::werefox_card::*; use crate::utils::prop_structs::*; use dioxus_router::prelude::*; #[derive(Routable, PartialEq, Clone)] pub enum Route { #[route("/")] #[redirect("/:..segments", |segments: Vec| Route::Home {})] Home {}, #[route("/projects")] Projects {}, #[route("/testimonials")] Testimonials {}, #[route("/hrt")] Hrt {}, #[route("/faq")] Faq {}, } pub fn DioxusApp() -> Element { rsx! { Router:: {} } } #[component] /// Renders the app and returns the rendered Element. pub fn Home() -> Element { let identity_list = [ ( "29", ImageProps { src: "/emoji/18_plus.svg".to_string(), alt: "Over 18 emoji".to_string(), }, ), ( "Transfemme", ImageProps { src: "/emoji/female_symbol.svg".to_string(), alt: "Female symbol emoji".to_string(), }, ), ( "She/It", ImageProps { src: "/emoji/speech_bubble_left.svg".to_string(), alt: "A speech bubble emoji".to_string(), }, ), ( "Pansexual", ImageProps { src: "/emoji/pansexual_flag.svg".to_string(), alt: "Pansexual flag emoji".to_string(), }, ), ( "Lesbian", ImageProps { src: "/emoji/lesbian_flag.svg".to_string(), alt: "Lesbian flag emoji".to_string(), }, ), ( "Dragon", ImageProps { src: "/emoji/:alicehappy:.png".to_string(), alt: "Alice happy emoji".to_string(), }, ), ]; let project_list = [ ( "Stuff I Do!", Route::Projects {}, ImageProps { src: "/emoji/crt_prompt.svg".to_string(), alt: "CRT prompt emoji".to_string(), }, ), ( "See Testimonials!", Route::Testimonials {}, ImageProps { src: "/emoji/awoo.svg".to_string(), alt: "Awoo emoji".to_string(), }, ), ( "HRT Tracker!", Route::Hrt {}, ImageProps { src: "/emoji/trans_heart.png".to_string(), alt: "Transgender heart emoji".to_string(), }, ), ( "FAQ", Route::Faq {}, ImageProps { src: "/emoji/red_question_mark.svg".to_string(), alt: "Red question mark emoji".to_string(), }, ), ]; rsx! { HomePage { page_title: "About a Werefox", card_title: "Hi! I'm Alice Werefox!", WerefoxCard { card_title: "Basic Info".to_string(), title_emoji: None, div { class: "grid grid-cols-1 gap-0 xl:grid-rows-1 xl:grid-cols-6 sm:grid-rows-2 sm:grid-cols-3 grid-rows-9 sm:gap-2", for e in identity_list { IdentityButton { button_text: e.0, image_props: e.1 } } } } WerefoxCard { card_title: "Welcome to my little info site!", title_emoji: None, IntroductionCard {} } WerefoxCard { card_title: "Neat Pages!", title_emoji: None, div { class: "flex flex-col space-y-4", for e in project_list { PageButton { button_text: e.0, button_route: e.1, image_props: e.2 } } } } } } } #[component] pub fn Projects() -> Element { let project_list = [ ( "Streaming", "I stream regularly! Sometimes randomizers, sometimes just a chill time with a fun game.", "https://twitch.tv/alice_werefox", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Poetry", "Sometimes, I write poetry. It's not always the happiest, but I am proud of it. It would mean a lot to me if you took a look!", "https://void.werefox.cafe", ImageProps { src: "/emoji/pen.svg".to_string(), alt: "Pen emoji".to_string(), }, ), ( "Valentine's Day Letter", "Here's a little treat I put together for Valentine's Day one year, and I decided to just keep it up all year. Feel free to take a look if you ever need a little pick-me-up.", "https://letter.werefox.cafe", ImageProps { src: "/emoji/red_heart.svg".to_string(), alt: "Red heart emoji".to_string(), }, ), ( "Programming", "Much like this site, sometimes I program things! Gotta at least try and put that Comp. Sci. degree to good use!", "https://gitea.werefox.cafe/ada", ImageProps { src: "/emoji/laptop.svg".to_string(), alt: "Laptop emoji".to_string(), }, ), ]; let service_list = [ ( "Nextcloud", None, "https://cloud.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Gitea", None, "https://gitea.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "GoToSocial", None, "https://gts.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Headscale", Some("(A self-hosted tailscale server)".to_string()), "https://headscale.net/", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Jellyfin", None, "https://watch.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Matrix", None, "https://matrix.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Navidrome", None, "https://music.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Tunic Transition Tracker", Some("(A tracker I made for a game I love)".to_string()), "https://tunic.werefox.cafe", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ]; let personal_list = [ ( "Cockpit", None, "https://cockpit-project.org/", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Dockge", None, "https://dockge.kuma.pet/", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Home Assistant", None, "https://www.home-assistant.io/", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ( "Pi-Hole", None, "https://pi-hole.net/", ImageProps { src: "/emoji/twitch-logo.png".to_string(), alt: "Twitch logo".to_string(), }, ), ]; rsx! { BasicPage { page_title: "Some stuff I do!", div { class: "rounded-lg ring-2 ring-alice-werefox-grey dark:ring-alice-werefox-grey-darker bg-alice-werefox-grey-light dark:bg-alice-werefox-grey", WerefoxCard { card_title: "Personal Projects", for project in project_list { ProjectCard { project_title: project.0, project_description: project.1, project_link: project.2, image_props: project.3 } } ProjectFoldable { project_title: "Services", project_description: "Click here for a list of the services I host.", image_props: ImageProps { src: "/emoji/crt_blue_screen.svg".to_string(), alt: "A CRT blue screen emoji.".to_string(), }, for service in service_list { ProjectCard { project_title: service.0, project_description: service.1, project_link: service.2, image_props: service.3 } } } ProjectFoldable { project_title: "Personal Use", project_description: "Click here for a list of services I have set up for just me.", image_props: ImageProps { src: "/emoji/crt_blue_screen.svg".to_string(), alt: "A CRT blue screen emoji.".to_string(), }, for personal in personal_list { ProjectCard { project_title: personal.0, project_description: personal.1, project_link: personal.2, image_props: personal.3 } } } } } } } } #[component] pub fn Testimonials() -> Element { let mastodon_status = use_resource(get_account_avatars); rsx! { BasicPage { page_title: "Testimonials!", WerefoxCard { card_title: "Sometimes, people say some nice things about me. Here are some examples!", div { class: "p-2 space-y-4", match (mastodon_status.value())() { Some(account) => rsx! { for a in account.unwrap() { div { class: "transition rounded-sm ring-2 ring-alice-werefox-red-dark dark:ring-alice-werefox-red hover:ring-alice-werefox-blue-dark dark:hover:ring-alice-werefox-blue text-alice-werefox-red-dark dark:text-alice-werefox-red-light hover:text-alice-werefox-blue-dark dark:hover:text-alice-werefox-blue-light hover:animate-yip", a { href: format!("{}", a.url.unwrap().trim_matches(|c| c == '\"')), target: "_blank", div { class: "flex flex-row min-w-full overflow-hidden", div { class: "order-1 inline-block object-contain pt-4 pb-4 pl-4", // span { class: "relative w-16 h-16 sm:w-32 sm:h-32", img { src: format!("{}", a.avatar.unwrap().trim_matches(|c| c == '\"')), class: "object-contain h-16 rounded-md sm:h-32", alt: "" } // } } div { class: "flex items-center justify-center order-2 w-full min-h-full p-4", div { class: "text-xs text-center animate-wiggle sm:text-lg", p { class: "overflow-auto", { format!("{}", a.content) } } { format!("{}", a.username) } } } } } } } }, _ => rsx! { div { class: "flex flex-col justify-center w-full m-auto space-y-2 align-middle", img { class: "flex justify-center m-auto align-middle", src: "emoji/:ablobalicebongo:.gif", alt: "A gif of Alice in a blob emoji form, performing the bongocat animation." } p { "Just a sec, lemme grab those for ya..." } } } } } } } } } #[server] async fn get_account_avatars() -> Result, ServerFnError> { let client = megalodon::generator( megalodon::SNS::Mastodon, String::from("https://bark.lgbt"), Some(tokio::fs::read_to_string("access_token.key").await?), None, ); let testimonial_toml: String = tokio::fs::read_to_string("data/testimonials.toml").await?; let testimonial_accounts: TestimonialAccount = toml::from_str(testimonial_toml.as_str()).unwrap(); let mut testimonial_avatars: Vec = vec![]; for account in testimonial_accounts.accounts { let res = client .search_account( String::from(account.username.clone()), Some(&megalodon::megalodon::SearchAccountInputOptions { following: None, resolve: Some(false), limit: Some(1), max_id: None, since_id: None, }), ) .await?; testimonial_avatars.push(Testimonial { username: account.username, content: account.content, avatar: Some(format!("{:?}", res.json()[0].avatar_static)), url: Some(format!("{:?}", res.json()[0].url)), }) } Ok(testimonial_avatars) } #[derive(Deserialize, Debug)] struct TestimonialAccount { accounts: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Testimonial { username: String, content: String, avatar: Option, url: Option, } #[component] pub fn Hrt() -> Element { let time_elapsed = use_signal(|| TimeElapsed { days: 0, hours: 0, minutes: 0, seconds: 0, }); use_coroutine(|_rx: UnboundedReceiver<()>| { to_owned![time_elapsed]; async move { loop { time_elapsed.set(get_hrt_time().await.unwrap()); } } }); rsx! { BasicPage { page_title: "Track my HRT progress!", WerefoxCard { card_title: "I'm so glad you're interested!!", div { class: "grid grid-cols-1 grid-rows-4 gap-2 p-2 text-lg text-center sm:text-xl text-alice-werefox-red-dark dark:text-alice-werefox-red-light", p { "I've been on HRT for:" } p { "{time_elapsed.read().days} days" } p { "{time_elapsed.read().hours} hours" } p { "{time_elapsed.read().minutes} minutes" } p { "{time_elapsed.read().seconds} seconds" } } } } } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TimeElapsed { days: i64, hours: i64, minutes: i64, seconds: i64, } #[server] async fn get_hrt_time() -> Result { use time::{macros::datetime, Duration, OffsetDateTime, UtcOffset}; let mut hrt_start_date: OffsetDateTime = datetime!(2020-12-11 0:00 UTC); // TODO: Fix this so that the offset time total matches the server's UTC offset. // let hrt_start_date_offset: UtcOffset = UtcOffset::local_offset_at(hrt_start_date).unwrap(); // hrt_start_date = hrt_start_date.replace_offset(hrt_start_date_offset); let now: OffsetDateTime = OffsetDateTime::now_utc(); let duration: Duration = now - hrt_start_date; Ok(TimeElapsed { days: duration.whole_days(), hours: duration.whole_hours() % 24, minutes: duration.whole_minutes() % 60, seconds: duration.whole_seconds() % 60, }) } #[component] pub fn Faq() -> Element { let faq_list = [( "So is Werefox like a species or...?", "That's a good question! No, my fursona's full name is Alice Icehart Werefox, Werefox is just a last name. I got it from playing Second Life back in the day.", ImageProps { src: "/images/AliceDefault.png".to_string(), alt: "Alice as an emoji".to_string(), }, ),( "How can you be Pansexual and a Lesbian?", "I believe I've been told the proper term is \"sapphic\", it just means I *am* Pansexual, but I tend to prefer those who identify more femme.", ImageProps { src: "/images/AliceHeartRainbow.png".to_string(), alt: "Alice with a gay pride heart".to_string(), }, ),( "How do I get those Xenia stickers?", "Yeah, about that. So, I've made a few posts about this, but when I first started giving those out, I was in a good financial position, among other things. Now I'm not! I will get to it when I do.", ImageProps { src: "/images/alice_Sweatdrop.svg".to_string(), alt: "Alice with a sweatdrop".to_string(), }, ),( "What do you do?", "Lots of things! If you want to know more about what I do, you can check out \"Stuff I do!\" from the main page, or \"Support Me?\" if you wanna toss a donation! I'm currently still unemployed, so any extra funds would be appreciated!", ImageProps { src: "/images/AliceEmojiAliceYay.png".to_string(), alt: "Happy Alice emoji".to_string(), }, )]; rsx! { BasicPage { page_title: "Here are some FAQs", div { class: "p-4 space-y-4 rounded-lg ring-2 ring-alice-werefox-grey dark:ring-alice-werefox-grey-darker bg-alice-werefox-grey-lightest dark:bg-alice-werefox-grey-dark", for faq in faq_list { FaqCard { question: faq.0, answer: faq.1, image_props: faq.2 } } } } } } }