diff --git a/.gitignore b/.gitignore index bd6126c..97ba621 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ pkg/ target/ Cargo.lock +bulma.min.css diff --git a/Cargo.toml b/Cargo.toml index e7f4173..05ee636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ wasm-bindgen = { version = "0.2" } log = { version = "0.4" } wasm-logger = { version = "0.2" } +serde = { version = "1.0" } + [dependencies.web-sys] version = "0.3" features = [ diff --git a/index.html b/index.html index 16f501a..abe66e0 100644 --- a/index.html +++ b/index.html @@ -29,10 +29,9 @@ + - - \ No newline at end of file diff --git a/justfile b/justfile index 5c8fd8f..48b4a71 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,9 @@ install: cargo install --locked wasm-pack trunk --force debug: + rm bulma.min.css && curl --proto '=https' --tlsv1.3 -LO https://cdn.jsdelivr.net/npm/bulma/css/bulma.min.css trunk serve build: + rm bulma.min.css && curl --proto '=https' --tlsv1.3 -LO https://cdn.jsdelivr.net/npm/bulma/css/bulma.min.css trunk build --release diff --git a/src/main.rs b/src/main.rs index aa72ff8..9a4a52f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,10 @@ use yew_router::prelude::*; mod pages; use crate::pages::about::About; +use crate::pages::blog::entries::Entries; +use crate::pages::blog::entry::Entry; +use crate::pages::blog::authors::Authors; +use crate::pages::blog::author::Author; use crate::pages::projects::Projects; use pages::not_found::PageNotFound; @@ -17,6 +21,15 @@ use pages::not_found::PageNotFound; pub enum Route { #[at("/")] About, + + #[at("/blog/entries/:id")] + Entry { id: u64 }, + #[at("/blog/entries")] + Entries, + #[at("/blog/authors/:id")] + Author { id: u64 }, + #[at("/blog/authors")] + Authors, #[at("/projects")] Projects, @@ -116,6 +129,12 @@ impl Component for App { fn switch(routes: Route) -> Html { match routes { Route::About => { html! { } } + + Route::Entries => { html! { } } + Route::Entry { id } => { html! { } } + Route::Authors => { html! { } } + Route::Author { id } => { html! { } } + Route::Projects => { html! { } } Route::NotFound => { html! { } } } @@ -146,6 +165,7 @@ impl App {
diff --git a/src/pages/blog/author.rs b/src/pages/blog/author.rs new file mode 100644 index 0000000..fdf0c72 --- /dev/null +++ b/src/pages/blog/author.rs @@ -0,0 +1,66 @@ +use yew::prelude::*; +use crate::pages::blog::content; +use crate::pages::blog::content::BlogEntry; + +#[derive(Clone, Debug, Eq, PartialEq, Properties)] +pub struct Props { + pub seed: u64, +} + +pub struct Author { + author: content::Author, +} +impl Component for Author { + type Message = (); + type Properties = Props; + + fn create(ctx: &Context) -> Self { + Self { author: content::Author::from_seed(ctx.props().seed), } + } + + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { + self.author = content::Author::from_seed(ctx.props().seed); + true + } + + fn view(&self, _ctx: &Context) -> Html { + let Self { author } = self; + + html! { +
+
+
+
+

{ &author.name }

+
+
+
+
+
+

{ "Interests" }

+
+ { for author.keywords.iter().map(|tag| html! { { tag } }) } +
+
+
+
+
+ The author's profile picture. +
+
+
+
+
+

{ "About me" }

+
+ { "This author has chosen not to reveal anything about themselves" } +
+
+
+
+
+
+
+ } + } +} \ No newline at end of file diff --git a/src/pages/blog/authorcard.rs b/src/pages/blog/authorcard.rs new file mode 100644 index 0000000..23615d9 --- /dev/null +++ b/src/pages/blog/authorcard.rs @@ -0,0 +1,59 @@ +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::content::Author; +use crate::generator::Generated; +use crate::Route; + +#[derive(Clone, Debug, PartialEq, Eq, Properties)] +pub struct Props { + pub seed: u64, +} + +pub struct AuthorCard { + author: Author, +} +impl Component for AuthorCard { + type Message = (); + type Properties = Props; + + fn create(ctx: &Context) -> Self { + Self { + author: Author::from_seed(ctx.props().seed), + } + } + + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { + self.author = Author::from_seed(ctx.props().seed); + true + } + + fn view(&self, _ctx: &Context) -> Html { + let Self { author } = self; + html! { +
+
+
+
+
+ Author's profile picture +
+
+
+

{ &author.name }

+

+ { "I like " } + { author.keywords.join(", ") } +

+
+
+
+
+ classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}> + { "Profile" } + > +
+
+ } + } +} \ No newline at end of file diff --git a/src/pages/blog/authors.rs b/src/pages/blog/authors.rs new file mode 100644 index 0000000..debcf71 --- /dev/null +++ b/src/pages/blog/authors.rs @@ -0,0 +1,37 @@ +use yew::prelude::*; + +use crate::pages::blog::content::AuthorCard; + +pub enum Msg { + NextAuthors, +} + +pub struct Authors { + seeds: Vec, +} +impl Component for Authors { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + Self { seeds: vec![1], } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { Msg::NextAuthors => { self.seeds = vec![1]; true } } + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
+ { for self.seeds.iter().map(|&seed| { html! { +
+
+ +
+
+ } }) } +
+ } + } +} diff --git a/src/pages/blog/content.rs b/src/pages/blog/content.rs new file mode 100644 index 0000000..a4ac299 --- /dev/null +++ b/src/pages/blog/content.rs @@ -0,0 +1,423 @@ +use std::rc::Rc; +use std::ops::Range; + +use serde::{Deserialize, Serialize}; +use yew::prelude::*; +use yew_router::prelude::*; +use yew_router::components::Link; + +use crate::Route; + +const ELLIPSIS: &str = "\u{02026}"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Author { + pub seed: u64, + pub name: String, + pub keywords: Vec, + pub image_url: String, +} +impl BlogEntry for Author { + fn from_entry(entry: &mut Entry) -> Self { + return match entry.seed { + 1 => Self {seed: entry.seed, name: "iouring".to_string(), keywords: vec!["meow".to_string(), "mrrp".to_string(), "mew".to_string()], image_url: "".to_string() }, + _ => Self {seed: entry.seed, name: "none".to_string(), keywords: vec!["meow".to_string(), "mrrp".to_string(), "mew".to_string()], image_url: "".to_string()} + }; + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PostMeta { + pub seed: u64, + pub title: String, + pub author: Author, + pub keywords: Vec, + pub image_url: String, +} +impl BlogEntry for PostMeta { + fn from_entry(entry: &mut Entry) -> Self { + return Self { + seed: entry.seed, + title: "awawa title".to_string(), + author: Author::from_entry(entry), + keywords: vec!["meow".to_string(), "mrrp".to_string(), "mew".to_string()], + image_url: "".to_string() + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Post { + pub meta: PostMeta, + pub content: Vec, +} +impl BlogEntry for Post { + fn from_entry(entry: &mut Entry) -> Self { + return Self { meta: PostMeta::from_entry(entry), content: vec![PostPart::from_entry(entry)] } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PostPart { + Section(Section), + Quote(Quote), +} +impl BlogEntry for PostPart { + fn from_entry(entry: &mut Entry) -> Self { + return Self::Quote(Quote::from_entry(entry)); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Section { + pub title: String, + pub paragraphs: Vec, + pub image_url: String, +} +impl BlogEntry for Section { + fn from_entry(entry: &mut Entry) -> Self { + return Self { title: "awawa title".to_string(), paragraphs: vec!["meow".to_string(), "mrrp".to_string(), "mew".to_string()], image_url: "".to_string() } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Quote { + pub author: Author, + pub content: String, +} +impl BlogEntry for Quote { + fn from_entry(entry: &mut Entry) -> Self { + return Self { author: Author::from_seed(entry.seed), content: "awawa content".to_string() } + } +} + +pub struct Entry { + pub seed: u64 +} + +impl Entry { + pub fn from_seed(seed: u64) -> Self { + Self { seed } + } +} + +pub trait BlogEntry: Sized { + fn from_entry(entry: &mut Entry) -> Self; + fn from_seed(seed: u64) -> Self { + Self::from_entry(&mut Entry::from_seed(seed)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Properties)] +pub struct PropsAuthorCard { + pub seed: u64, +} + +pub struct AuthorCard { + author: Author, +} +impl Component for AuthorCard { + type Message = (); + type Properties = PropsAuthorCard; + + fn create(ctx: &Context) -> Self { + Self { + author: Author::from_seed(ctx.props().seed), + } + } + + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { + self.author = Author::from_seed(ctx.props().seed); + true + } + + fn view(&self, _ctx: &Context) -> Html { + let Self { author } = self; + html! { +
+
+
+
+
+ this should normally show an image mew,, +
+
+
+

{ &author.name }

+

+ { "I like " } + { author.keywords.join(", ") } +

+
+
+
+
+ classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}> + { "Profile" } + > +
+
+ } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Properties)] +pub struct PropsPostCard { + pub seed: u64, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct PostMetaState { + inner: PostMeta, +} + +impl Reducible for PostMetaState { + type Action = u64; + + fn reduce(self: Rc, action: u64) -> Rc { + Self { inner: PostMeta::from_seed(action.into()), }.into() + } +} + +#[function_component] +pub fn PostCard(props: &PropsPostCard) -> Html { + let seed = props.seed; + + let post = use_reducer_eq(|| PostMetaState { + inner: PostMeta::from_seed(seed.into()), + }); + + { + let post_dispatcher = post.dispatcher(); + use_effect_with(seed, move |seed| { + post_dispatcher.dispatch(*seed); + + || {} + }); + } + + let post = &post.inner; + + html! { +
+
+
+ This post's image +
+
+
+ classes={classes!("title", "is-block")} to={Route::Entry { id: post.seed }}> + { &post.title } + > + classes={classes!("subtitle", "is-block")} to={Route::Author { id: post.author.seed }}> + { &post.author.name } + > +
+
+ } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct PageQuery { + pub page: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Properties)] +pub struct PropsNavigation { + pub page: u64, + pub total_pages: u64, + pub route_to_page: Route, +} + +#[function_component] +pub fn RelNavButtons(props: &PropsNavigation) -> Html { + let PropsNavigation { + page, + total_pages, + route_to_page: to, + } = props.clone(); + + html! { + <> + + classes={classes!("pagination-previous")} + disabled={page==1} + query={Some(PageQuery{page: page - 1})} + to={to.clone()} + > + { "Previous" } + > + + classes={classes!("pagination-next")} + disabled={page==total_pages} + query={Some(PageQuery{page: page + 1})} + {to} + > + { "Next page" } + > + + } +} + +#[derive(Properties, Clone, Debug, PartialEq, Eq)] +pub struct RenderLinksProps { + range: Range, + len: usize, + max_links: usize, + props: PropsNavigation, +} + +#[function_component] +pub fn RenderLinks(props: &RenderLinksProps) -> Html { + let RenderLinksProps { + range, + len, + max_links, + props, + } = props.clone(); + + let mut range = range; + + if len > max_links { + let last_link = + html! {}; + // remove 1 for the ellipsis and 1 for the last link + let links = range + .take(max_links - 2) + .map(|page| html! {}); + html! { + <> + { for links } +
  • { ELLIPSIS }
  • + { last_link } + + } + } else { + html! { + for page in range { + + } + } + } +} + +#[derive(Properties, Clone, Debug, PartialEq, Eq)] +pub struct RenderLinkProps { + to_page: u64, + props: PropsNavigation, +} + +#[function_component] +pub fn RenderLink(props: &RenderLinkProps) -> Html { + let RenderLinkProps { to_page, props } = props.clone(); + + let PropsNavigation { + page, + route_to_page, + .. + } = props; + + let is_current_class = if to_page == page { "is-current" } else { "" }; + + html! { +
  • + + classes={classes!("pagination-link", is_current_class)} + to={route_to_page} + query={Some(PageQuery{page: to_page})} + > + { to_page } + > +
  • + } +} + +#[function_component] +pub fn Links(props: &PropsNavigation) -> Html { + const LINKS_PER_SIDE: usize = 3; + + let PropsNavigation { + page, total_pages, .. + } = *props; + + let pages_prev = page.checked_sub(1).unwrap_or_default() as usize; + let pages_next = (total_pages - page) as usize; + + let links_left = LINKS_PER_SIDE.min(pages_prev) + // if there are less than `LINKS_PER_SIDE` to the right, we add some more on the left. + + LINKS_PER_SIDE.checked_sub(pages_next).unwrap_or_default(); + let links_right = 2 * LINKS_PER_SIDE - links_left; + + html! { + <> + + + + + } +} + +#[function_component] +pub fn Pagination(props: &PropsNavigation) -> Html { + html! { + + } +} + +#[function_component] +pub fn Nav() -> Html { + let navbar_active = use_state_eq(|| false); + + let toggle_navbar = { + let navbar_active = navbar_active.clone(); + + Callback::from(move |_| { + navbar_active.set(!*navbar_active); + }) + }; + + let active_class = if !*navbar_active { "is-active" } else { "" }; + + html! { + + } +} diff --git a/src/pages/blog/entries.rs b/src/pages/blog/entries.rs new file mode 100644 index 0000000..0745823 --- /dev/null +++ b/src/pages/blog/entries.rs @@ -0,0 +1,92 @@ +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::pages::blog::content::PostCard; +use crate::pages::blog::content::{PageQuery, Pagination}; +use crate::Route; + +const ITEMS_PER_PAGE: u64 = 10; +const TOTAL_PAGES: u64 = u64::MAX / ITEMS_PER_PAGE; + +pub enum Msg { + PageUpdated, +} + +pub struct Entries { + page: u64, + _listener: LocationHandle, +} + +fn current_page(ctx: &Context) -> u64 { + let location = ctx.link().location().unwrap(); + + location.query::().map(|it| it.page as u64).unwrap_or(1) +} + +impl Component for Entries { + type Message = Msg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + let link = ctx.link().clone(); + let listener = ctx + .link() + .add_location_listener(link.callback(move |_| Msg::PageUpdated)) + .unwrap(); + + Self { + page: current_page(ctx), + _listener: listener, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::PageUpdated => self.page = current_page(ctx), + } + true + } + + fn view(&self, ctx: &Context) -> Html { + let page = self.page; + + html! { +
    +

    { "Posts" }

    +

    { "All of our quality writing in one place" }

    + { self.view_posts(ctx) } + +
    + } + } +} +impl Entries { + fn view_posts(&self, _ctx: &Context) -> Html { + let start_seed = (self.page - 1) * ITEMS_PER_PAGE; + let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| { + html! { +
  • + +
  • + } + }); + html! { +
    +
    +
      + { for cards.by_ref().take(ITEMS_PER_PAGE as usize / 2) } +
    +
    +
    +
      + { for cards } +
    +
    +
    + } + } +} \ No newline at end of file diff --git a/src/pages/blog/entry.rs b/src/pages/blog/entry.rs new file mode 100644 index 0000000..005e969 --- /dev/null +++ b/src/pages/blog/entry.rs @@ -0,0 +1,152 @@ +use std::rc::Rc; + +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::Route; +use crate::pages::blog::content; +use crate::pages::blog::content::BlogEntry; + +#[derive(Clone, Debug, Eq, PartialEq, Properties)] +pub struct Props { + pub seed: u32, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct PostState { + pub inner: content::Post, +} + +impl Reducible for PostState { + type Action = u32; + fn reduce(self: Rc, action: u32) -> Rc { + Self { inner: content::Post::from_seed(action.into()), }.into() + } +} + +#[function_component] +pub fn Entry(props: &Props) -> Html { + let seed = props.seed; + + let post = use_reducer(|| PostState { + inner: content::Post::from_seed(seed.into()), + }); + + { + let post_dispatcher = post.dispatcher(); + use_effect_with(seed, move |seed| { + post_dispatcher.dispatch(*seed); + + || {} + }); + } + + let post = &post.inner; + + let render_quote = |quote: &content::Quote| { + html! { +
    +
    +

    + The author's profile +

    +
    +
    +
    + classes={classes!("is-size-5")} to={Route::Author { id: quote.author.seed }}> + { "e.author.name } + > +

    + { "e.content } +

    +
    +
    +
    + } + }; + + let render_section_hero = |section: &content::Section| { + html! { +
    + This section's image +
    +
    +

    { §ion.title }

    +
    +
    +
    + } + }; + + let render_section = |section, show_hero| { + let hero = if show_hero { + render_section_hero(section) + } else { + html! {} + }; + let paragraphs = section.paragraphs.iter().map(|paragraph| { + html! { +

    { paragraph }

    + } + }); + html! { +
    + { hero } +
    { for paragraphs }
    +
    + } + }; + + let view_content = { + // don't show hero for the first section + let mut show_hero = false; + + let parts = post.content.iter().map(|part| match part { + content::PostPart::Section(section) => { + let html = render_section(section, show_hero); + // show hero between sections + show_hero = true; + html + } + content::PostPart::Quote(quote) => { + // don't show hero after a quote + show_hero = false; + render_quote("e) + } + }); + html! {{for parts}} + }; + + let keywords = post + .meta + .keywords + .iter() + .map(|keyword| html! { { keyword } }); + + html! { + <> +
    + The hero's background +
    +
    +

    + { &post.meta.title } +

    +

    + { "by " } + classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}> + { &post.meta.author.name } + > +

    +
    + { for keywords } +
    +
    +
    +
    +
    + { view_content } +
    + + } +} \ No newline at end of file diff --git a/src/pages/blog/mod.rs b/src/pages/blog/mod.rs new file mode 100644 index 0000000..12ceea5 --- /dev/null +++ b/src/pages/blog/mod.rs @@ -0,0 +1,5 @@ +pub mod content; +pub mod entries; +pub mod entry; +pub mod authors; +pub mod author; \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 7fcee1a..83de40a 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,4 +1,5 @@ pub mod about; +pub mod blog; pub mod projects; pub mod not_found; diff --git a/src/pages/projects.rs b/src/pages/projects.rs index 1ad0f74..3f2ddef 100644 --- a/src/pages/projects.rs +++ b/src/pages/projects.rs @@ -27,13 +27,13 @@ impl Projects {

    {r#"ongoing projects"#}

    -

    +

    {r#"catnip"#}

    {r#"all-rounder ide in the making"#}

    -

    +

    {r#"neko chat"#}

    {r#"my planned matrix client"#}

    @@ -49,25 +49,25 @@ impl Projects {

    {r#"finished projects"#}

    -

    +

    {r#"tiny events"#}

    {r#"a java seventeen (and up) event-sys that is able to be scaled in large systems."#}

    -

    +

    {r#"dotfiles"#}

    {r#"my personal set of configurations for different things"#}

    -

    +

    {r#"swingify"#}

    {r#"my java swing wrapper to simplify window creation for bogus-brains."#}

    -

    +

    {r#"modern netty"#}

    {r#"fabric mod that adds experimental iouring and kqueue support."#}