547 lines
22 KiB
Rust
547 lines
22 KiB
Rust
//! # 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<String>| Route::Home {})]
|
|
Home {},
|
|
#[route("/projects")]
|
|
Projects {},
|
|
#[route("/testimonials")]
|
|
Testimonials {},
|
|
#[route("/hrt")]
|
|
Hrt {},
|
|
#[route("/faq")]
|
|
Faq {},
|
|
}
|
|
pub fn DioxusApp() -> Element {
|
|
rsx! { Router::<Route> {} }
|
|
}
|
|
|
|
#[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 grid-rows-6 gap-0 xl:grid-rows-1 xl:grid-cols-6 sm:grid-rows-2 sm:grid-cols-3 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<Vec<Testimonial>, ServerFnError> {
|
|
let mut access_token = tokio::fs::read_to_string("access_token.key").await?;
|
|
access_token = access_token.trim().to_string();
|
|
let client = megalodon::generator(
|
|
megalodon::SNS::Mastodon,
|
|
String::from("https://yiff.life"),
|
|
Some(access_token),
|
|
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<Testimonial> = 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<Testimonial>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct Testimonial {
|
|
username: String,
|
|
content: String,
|
|
avatar: Option<String>,
|
|
url: Option<String>,
|
|
}
|
|
|
|
#[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<TimeElapsed, ServerFnError> {
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|