sloppy blog impl from example

This commit is contained in:
lia
2025-08-10 18:28:27 +02:00
parent 6a5176caab
commit 078634b664
14 changed files with 867 additions and 8 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ dist/
pkg/
target/
Cargo.lock
bulma.min.css

View File

@@ -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 = [

View File

@@ -29,10 +29,9 @@
<link data-trunk rel="copy-file" href="public/buttons/aqueer.png" />
<link data-trunk rel="sass" href="main.scss" />
<link data-trunk rel="css" href="bulma.min.css" />
<link data-trunk rel="rust" />
<base data-trunk-public-url />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma/css/bulma.min.css" />
</head>
</html>

View File

@@ -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

View File

@@ -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;
@@ -18,6 +22,15 @@ 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! { <About /> } }
Route::Entries => { html! { <Entries /> } }
Route::Entry { id } => { html! { <Entry seed={id as u32} /> } }
Route::Authors => { html! { <Authors /> } }
Route::Author { id } => { html! { <Author seed={id} /> } }
Route::Projects => { html! { <Projects /> } }
Route::NotFound => { html! { <PageNotFound /> } }
}
@@ -146,6 +165,7 @@ impl App {
<div class={classes!("navbar-menu", active_class)}>
<div class="navbar-start">
<Link<Route> classes={classes!("navbar-item")} to={Route::About}>{r"about"}</Link<Route>>
<Link<Route> classes={classes!("navbar-item")} to={Route::Entries}>{r"blog"}</Link<Route>>
<Link<Route> classes={classes!("navbar-item")} to={Route::Projects}>{r"projects"}</Link<Route>>
</div>
</div>

66
src/pages/blog/author.rs Normal file
View File

@@ -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 {
Self { author: content::Author::from_seed(ctx.props().seed), }
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.author = content::Author::from_seed(ctx.props().seed);
true
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let Self { author } = self;
html! {
<div class="section container">
<div class="tile is-ancestor is-vertical">
<div class="tile is-parent">
<article class="tile is-child notification is-light">
<p class="title">{ &author.name }</p>
</article>
</div>
<div class="tile">
<div class="tile is-parent is-3">
<article class="tile is-child notification">
<p class="title">{ "Interests" }</p>
<div class="tags">
{ for author.keywords.iter().map(|tag| html! { <span class="tag is-info">{ tag }</span> }) }
</div>
</article>
</div>
<div class="tile is-parent">
<figure class="tile is-child image is-square">
<img alt="The author's profile picture." src={author.image_url.clone()} />
</figure>
</div>
<div class="tile is-parent">
<article class="tile is-child notification is-info">
<div class="content">
<p class="title">{ "About me" }</p>
<div class="content">
{ "This author has chosen not to reveal anything about themselves" }
</div>
</div>
</article>
</div>
</div>
</div>
</div>
}
}
}

View File

@@ -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 {
Self {
author: Author::from_seed(ctx.props().seed),
}
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.author = Author::from_seed(ctx.props().seed);
true
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let Self { author } = self;
html! {
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-128x128">
<img alt="Author's profile picture" src={author.image_url.clone()} />
</figure>
</div>
<div class="media-content">
<p class="title is-3">{ &author.name }</p>
<p>
{ "I like " }
<b>{ author.keywords.join(", ") }</b>
</p>
</div>
</div>
</div>
<footer class="card-footer">
<Link<Route> classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}>
{ "Profile" }
</Link<Route>>
</footer>
</div>
}
}
}

37
src/pages/blog/authors.rs Normal file
View File

@@ -0,0 +1,37 @@
use yew::prelude::*;
use crate::pages::blog::content::AuthorCard;
pub enum Msg {
NextAuthors,
}
pub struct Authors {
seeds: Vec<u64>,
}
impl Component for Authors {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self { seeds: vec![1], }
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { Msg::NextAuthors => { self.seeds = vec![1]; true } }
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div class="container section tile is-ancestor">
{ for self.seeds.iter().map(|&seed| { html! {
<div class="tile is-parent">
<div class="tile is-child">
<AuthorCard {seed} />
</div>
</div>
} }) }
</div>
}
}
}

423
src/pages/blog/content.rs Normal file
View File

@@ -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<String>,
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<String>,
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<PostPart>,
}
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<String>,
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 {
Self {
author: Author::from_seed(ctx.props().seed),
}
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.author = Author::from_seed(ctx.props().seed);
true
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let Self { author } = self;
html! {
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-128x128">
<img alt="this should normally show an image mew,," src={author.image_url.clone()} />
</figure>
</div>
<div class="media-content">
<p class="title is-3">{ &author.name }</p>
<p>
{ "I like " }
<b>{ author.keywords.join(", ") }</b>
</p>
</div>
</div>
</div>
<footer class="card-footer">
<Link<Route> classes={classes!("card-footer-item")} to={Route::Author { id: author.seed }}>
{ "Profile" }
</Link<Route>>
</footer>
</div>
}
}
}
#[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<Self>, action: u64) -> Rc<Self> {
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! {
<div class="card">
<div class="card-image">
<figure class="image is-2by1">
<img alt="This post's image" src={post.image_url.clone()} loading="lazy" />
</figure>
</div>
<div class="card-content">
<Link<Route> classes={classes!("title", "is-block")} to={Route::Entry { id: post.seed }}>
{ &post.title }
</Link<Route>>
<Link<Route> classes={classes!("subtitle", "is-block")} to={Route::Author { id: post.author.seed }}>
{ &post.author.name }
</Link<Route>>
</div>
</div>
}
}
#[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! {
<>
<Link<Route, PageQuery>
classes={classes!("pagination-previous")}
disabled={page==1}
query={Some(PageQuery{page: page - 1})}
to={to.clone()}
>
{ "Previous" }
</Link<Route, PageQuery>>
<Link<Route, PageQuery>
classes={classes!("pagination-next")}
disabled={page==total_pages}
query={Some(PageQuery{page: page + 1})}
{to}
>
{ "Next page" }
</Link<Route, PageQuery>>
</>
}
}
#[derive(Properties, Clone, Debug, PartialEq, Eq)]
pub struct RenderLinksProps {
range: Range<u64>,
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! {<RenderLink to_page={range.next_back().unwrap()} props={props.clone()} />};
// remove 1 for the ellipsis and 1 for the last link
let links = range
.take(max_links - 2)
.map(|page| html! {<RenderLink to_page={page} props={props.clone()} />});
html! {
<>
{ for links }
<li><span class="pagination-ellipsis">{ ELLIPSIS }</span></li>
{ last_link }
</>
}
} else {
html! {
for page in range {
<RenderLink to_page={page} props={props.clone()} />
}
}
}
}
#[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! {
<li>
<Link<Route, PageQuery>
classes={classes!("pagination-link", is_current_class)}
to={route_to_page}
query={Some(PageQuery{page: to_page})}
>
{ to_page }
</Link<Route, PageQuery>>
</li>
}
}
#[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! {
<>
<RenderLinks range={ 1..page } len={pages_prev} max_links={links_left} props={props.clone()} />
<RenderLink to_page={page} props={props.clone()} />
<RenderLinks range={ page + 1..total_pages + 1 } len={pages_next} max_links={links_right} props={props.clone()} />
</>
}
}
#[function_component]
pub fn Pagination(props: &PropsNavigation) -> Html {
html! {
<nav class="pagination is-right" role="navigation" aria-label="pagination">
<RelNavButtons ..{props.clone()} />
<ul class="pagination-list">
<Links ..{props.clone()} />
</ul>
</nav>
}
}
#[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! {
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<h1 class="navbar-item is-size-3">{ "Yew Blog" }</h1>
<button class={classes!("navbar-burger", "burger", active_class)}
aria-label="menu" aria-expanded="false"
onclick={toggle_navbar}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
</div>
<div class={classes!("navbar-menu", active_class)}>
<div class="navbar-start">
<Link<Route> classes={classes!("navbar-item")} to={Route::Entries}> // TODO: ...
{ "Home" }
</Link<Route>>
<Link<Route> classes={classes!("navbar-item")} to={Route::Entries}>
{ "Posts" }
</Link<Route>>
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-link">
{ "More" }
</div>
<div class="navbar-dropdown">
<Link<Route> classes={classes!("navbar-item")} to={Route::Authors}>
{ "Meet the authors" }
</Link<Route>>
</div>
</div>
</div>
</div>
</nav>
}
}

92
src/pages/blog/entries.rs Normal file
View File

@@ -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<Entries>) -> u64 {
let location = ctx.link().location().unwrap();
location.query::<PageQuery>().map(|it| it.page as u64).unwrap_or(1)
}
impl Component for Entries {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> 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<Self>, msg: Self::Message) -> bool {
match msg {
Msg::PageUpdated => self.page = current_page(ctx),
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let page = self.page;
html! {
<div class="section container">
<h1 class="title">{ "Posts" }</h1>
<h2 class="subtitle">{ "All of our quality writing in one place" }</h2>
{ self.view_posts(ctx) }
<Pagination
{page}
total_pages={TOTAL_PAGES}
route_to_page={Route::Entries}
/>
</div>
}
}
}
impl Entries {
fn view_posts(&self, _ctx: &Context<Self>) -> Html {
let start_seed = (self.page - 1) * ITEMS_PER_PAGE;
let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| {
html! {
<li class="list-item mb-5">
<PostCard seed={start_seed + seed_offset} />
</li>
}
});
html! {
<div class="columns">
<div class="column">
<ul class="list">
{ for cards.by_ref().take(ITEMS_PER_PAGE as usize / 2) }
</ul>
</div>
<div class="column">
<ul class="list">
{ for cards }
</ul>
</div>
</div>
}
}
}

152
src/pages/blog/entry.rs Normal file
View File

@@ -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<Self>, action: u32) -> Rc<Self> {
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! {
<article class="media block box my-6">
<figure class="media-left">
<p class="image is-64x64">
<img alt="The author's profile" src={quote.author.image_url.clone()} loading="lazy" />
</p>
</figure>
<div class="media-content">
<div class="content">
<Link<Route> classes={classes!("is-size-5")} to={Route::Author { id: quote.author.seed }}>
<strong>{ &quote.author.name }</strong>
</Link<Route>>
<p class="is-family-secondary">
{ &quote.content }
</p>
</div>
</div>
</article>
}
};
let render_section_hero = |section: &content::Section| {
html! {
<section class="hero is-dark has-background mt-6 mb-3">
<img alt="This section's image" class="hero-background is-transparent" src={section.image_url.clone()} loading="lazy" />
<div class="hero-body">
<div class="container">
<h2 class="subtitle">{ &section.title }</h2>
</div>
</div>
</section>
}
};
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! {
<p>{ paragraph }</p>
}
});
html! {
<section>
{ hero }
<div>{ for paragraphs }</div>
</section>
}
};
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(&quote)
}
});
html! {{for parts}}
};
let keywords = post
.meta
.keywords
.iter()
.map(|keyword| html! { <span class="tag is-info">{ keyword }</span> });
html! {
<>
<section class="hero is-medium is-light has-background">
<img alt="The hero's background" class="hero-background is-transparent" src={post.meta.image_url.clone()} />
<div class="hero-body">
<div class="container">
<h1 class="title">
{ &post.meta.title }
</h1>
<h2 class="subtitle">
{ "by " }
<Link<Route> classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}>
{ &post.meta.author.name }
</Link<Route>>
</h2>
<div class="tags">
{ for keywords }
</div>
</div>
</div>
</section>
<div class="section container">
{ view_content }
</div>
</>
}
}

5
src/pages/blog/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod content;
pub mod entries;
pub mod entry;
pub mod authors;
pub mod author;

View File

@@ -1,4 +1,5 @@
pub mod about;
pub mod blog;
pub mod projects;
pub mod not_found;

View File

@@ -27,13 +27,13 @@ impl Projects {
<p class="title ml-2 is-size-4">{r#"ongoing projects"#}</p>
<div class="columns is-size-7 ml-3 mr-3" style="margin-top: var(--bulma-block-spacing) !important;">
<div class="column box mb-5 mr-1">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/app_catnip">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/i0uring/app_catnip">
{r#"catnip"#}
</a></p>
<p class="content">{r#"all-rounder ide in the making"#}</p>
</div>
<div class="column box mb-5 ml-1">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/app_nekochat">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/i0uring/app_nekochat">
{r#"neko chat"#}
</a></p>
<p class="content">{r#"my planned matrix client"#}</p>
@@ -49,25 +49,25 @@ impl Projects {
<p class="title ml-2 is-size-4">{r#"finished projects"#}</p>
<div class="columns is-size-7 ml-3 mr-3">
<div class="column box mb-5 mr-1">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/lib_tinyevents">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/i0uring/lib_tinyevents">
{r#"tiny events"#}
</a></p>
<p class="content">{r#"a java seventeen (and up) event-sys that is able to be scaled in large systems."#}</p>
</div>
<div class="column box mb-5 ml-1 mr-2">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/dotfiles">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/i0uring/dotfiles">
{r#"dotfiles"#}
</a></p>
<p class="content">{r#"my personal set of configurations for different things"#}</p>
</div>
<div class="column box mb-5 mr-1">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/lib_swingify">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/i0uring/lib_swingify">
{r#"swingify"#}
</a></p>
<p class="content">{r#"my java swing wrapper to simplify window creation for bogus-brains."#}</p>
</div>
<div class="column box mb-5 ml-1">
<p class="subtitle is-inline-block mb-0"><a href="https://git.gay/luciel/mc_mod_mnet">
<p class="subtitle is-inline-block mb-0"><a href="https://git.celesteflare.cc/stellaris/mod_mnet">
{r#"modern netty"#}
</a></p>
<p class="content">{r#"fabric mod that adds experimental iouring and kqueue support."#}</p>