🎨 based
Some checks failed
ci/woodpecker/push/build Pipeline failed

This commit is contained in:
JMARyA 2025-02-07 21:09:54 +01:00
parent fd2c9457d1
commit bf5cb72c16
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
10 changed files with 2214 additions and 300 deletions

2111
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,3 +8,5 @@ maud = "0.26.0"
rocket = "0.5.1"
serde = { version = "1.0.210", features = ["derive"] }
serde_yml = "0.0.12"
toml = "0.8.19"
based = { git = "https://git.hydrar.de/jmarya/based", branch = "ui" }

29
config.toml Normal file
View file

@ -0,0 +1,29 @@
# Project Definition
[project]
# Project Name
name = "Root Project"
# Project Description
description = "Root Project"
# Project Icon
icon = "icon.png"
# Project Website
website = "https://example.com"
# Project Documentation
documentation = "https://docs.example.com"
# Project Start
since = "1999-00-00"
# Contact Information
# eMail
contact.email = "mail@example.com"
# Subprojects
[project.sub.name]
name = "Sub Project"
description = "Sub Project"

View file

@ -1,13 +0,0 @@
projects:
root:
name: "Root Project"
description: "Root Project"
website: "https://example.com"
documentation: "https://docs.example.com"
since: "1999-00-00"
contact:
email: "mail@example.com"
sub:
sub_project:
name: "Sub Project"
description: "Sub Project"

View file

@ -1,11 +1,10 @@
use serde::Deserialize;
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub projects: HashMap<String, Project>,
}
pub struct Config {}
pub type ProjectConfig = HashMap<String, Project>;
#[derive(Debug, Clone, Deserialize)]
pub struct Project {
@ -16,7 +15,7 @@ pub struct Project {
pub documentation: Option<String>,
pub since: Option<String>,
pub contact: Option<ContactInfo>,
pub sub: Option<HashMap<String, Project>>,
pub sub: Option<ProjectConfig>,
}
#[derive(Debug, Clone, Deserialize)]
@ -24,58 +23,9 @@ pub struct ContactInfo {
pub email: Option<String>,
}
impl ContactInfo {
pub fn build(&self) -> maud::PreEscaped<String> {
maud::html!(
@if let Some(mail) = &self.email {
h3 { "Contact:" };
p { "Mail: "; a href=(format!("mailto:{mail}")) { (mail) }};
}
)
}
}
impl Config {
pub fn load(path: &str) -> Self {
pub fn load(path: &str) -> ProjectConfig {
let content = std::fs::read_to_string(path).unwrap();
serde_yml::from_str(&content).unwrap()
}
}
impl Project {
pub fn build(&self, card_id: &str) -> maud::PreEscaped<String> {
let subcard_id = format!("{card_id}_sub");
maud::html!(
div class="card" id=(card_id) {
@if let Some(icon) = &self.icon {
img src=(format!("/static/{}", icon)) style="float: left; width: 200px; height: 200px;margin-right: 80px;";
}
h3 { (self.name) };
p { (self.description) };
@if let Some(website) = &self.website {
p { "Website: "; a href=(website) { (website) }};
}
@if let Some(doc) = &self.documentation {
p { "Documentation: "; a href=(doc) { (doc) }};
}
@if let Some(since) = &self.since {
p { "Started: "; a href=(since) { (since) }};
}
@if let Some(contact) = &self.contact {
(contact.build())
}
@if self.sub.as_ref().map(|x| !x.is_empty()).unwrap_or(false) {
button class="expand-button" onclick=(format!("toggleSubcards('{subcard_id}')")) { "Expand" };
}
div class="subcards" id=(subcard_id) {
@if let Some(sub) = &self.sub {
@for (id, prj) in sub {
(prj.build(id))
}
}
};
};
)
toml::from_str(&content).unwrap()
}
}

10
src/invest.rs Normal file
View file

@ -0,0 +1,10 @@
// TODO : Investment feature
// TODO : Reoccuring investments
pub struct Investment {
pub project: String,
pub amount: f64,
pub what: String,
pub why: String,
pub when: String,
}

View file

@ -1,13 +1,73 @@
use std::path::Path;
use config::Config;
use based::asset::AssetRoutes;
use based::page;
use based::request::{RequestContext, StringResponse};
use based::ui::components::{Shell, Tabs};
use based::ui::prelude::*;
use based::ui::primitives::flex::Row;
use based::ui::primitives::Optional;
use config::{Project, ProjectConfig};
use maud::Render;
use rocket::fs::NamedFile;
use rocket::response::content::RawHtml;
use rocket::request::FromSegments;
use rocket::{get, launch, routes, State};
use site::gen_shell;
mod config;
mod invest;
mod site;
pub struct PathSegment {
pub segments: Vec<String>,
}
impl PathSegment {
pub fn to_str(&self) -> String {
self.segments.join("/")
}
}
impl<'r> FromSegments<'r> for PathSegment {
type Error = ();
fn from_segments(
segments: rocket::http::uri::Segments<'r, rocket::http::uri::fmt::Path>,
) -> Result<Self, Self::Error> {
let paths: Vec<_> = segments
.filter_map(|x| {
if x == "." {
return None;
}
if x == ".." {
return None;
}
Some(x.to_string())
})
.collect();
Ok(PathSegment { segments: paths })
}
}
#[get("/prj/<path..>")]
pub async fn project_page(
path: PathSegment,
ctx: RequestContext,
shell: &State<Shell>,
c: &State<ProjectConfig>,
) -> Option<StringResponse> {
let mut prj = c.get(path.segments.first()?);
for p in &path.segments[1..] {
println!(" --> {p}");
prj = prj.as_ref()?.sub.as_ref()?.get(p);
}
Some(render_project(shell, ctx, prj?, path.to_str()).await)
}
#[get("/static/<file>")]
async fn icon_res(file: &str) -> Option<NamedFile> {
let path = Path::new("static").join(file);
@ -15,8 +75,119 @@ async fn icon_res(file: &str) -> Option<NamedFile> {
}
#[get("/")]
pub fn main_page(c: &State<Config>) -> RawHtml<String> {
RawHtml(site::gen_site(c))
pub async fn main_page(
c: &State<ProjectConfig>,
ctx: RequestContext,
shell: &State<Shell>,
) -> StringResponse {
if c.len() == 1 {
let keys = c.keys().collect::<Vec<_>>();
let first = keys.first().unwrap();
let prj = c.get(*first).unwrap();
render_project(shell, ctx, prj, format!("{first}/")).await
} else {
// TODO : root project overview
site::gen_site(c, ctx).await
}
}
pub async fn render_project(
shell: &State<Shell>,
ctx: RequestContext,
prj: &Project,
root: String,
) -> StringResponse {
let title = format!("{} - Umbrella ☂️", prj.name);
page!(
shell,
ctx,
title,
Div()
.vanish()
.push(
Margin(
Row(vec![
Optional(prj.icon.as_ref(), |icon| Image(icon).alt("Project Icon")),
Text(&prj.name)._2xl().render()
])
.gap(ScreenValue::_4)
.full_center()
)
.top(ScreenValue::_6)
.bottom(ScreenValue::_2)
)
.push(
Padding(
Tabs()
.add_tab(
"info",
Text("📜 Info").medium(),
Flex(
Div()
.vanish()
.push(Text(&prj.description))
.push(Optional(prj.website.as_deref(), |website| Link(
website,
Text(&format!("Website: {website}"))
)))
.push(Optional(prj.documentation.as_deref(), |docs| Link(
docs,
Text(&format!("Documentation: {docs}"))
)))
.push(Optional(prj.since.as_deref(), |since| Text(&format!(
"Since: {since}"
))))
.push(Optional(prj.contact.as_ref(), |contact| {
Div().vanish().push(Text("Contact").large()).push(Text(
&format!(
"eMail: {}",
contact
.email
.as_ref()
.map(|x| x.as_str())
.unwrap_or_default()
),
))
}))
.push(Optional(prj.sub.as_ref(), |sub| {
let mut prj_links = Vec::new();
for key in sub.keys() {
prj_links.push(key);
}
Div()
.vanish()
.push(Text("Sub projects").large())
.push_for_each(&prj_links, |link: &_| {
Link(
&format!("/prj/{root}{link}"),
Text(
&prj.sub
.as_ref()
.unwrap()
.get(*link)
.unwrap()
.name,
),
)
})
}))
)
.gap(ScreenValue::_2)
.direction(Direction::Column)
)
.add_tab(
"invest",
Text("💵 Invest").medium(),
Div().vanish().push(Text("TODO"))
)
)
.x(ScreenValue::_6)
)
)
}
// todo : fav icon
@ -26,10 +197,14 @@ async fn rocket() -> _ {
let conf_path: String = std::env::args()
.skip(1)
.next()
.unwrap_or("./config.yml".to_string());
.unwrap_or("./config.toml".to_string());
let conf = config::Config::load(&conf_path);
let shell = gen_shell();
rocket::build()
.mount("/", routes![main_page, icon_res])
.mount_assets()
.mount("/", routes![main_page, icon_res, project_page])
.manage(conf)
.manage(shell)
}

View file

@ -1,8 +0,0 @@
function toggleSubcards(id) {
var subcards = document.getElementById(id);
if (subcards.style.display === "block") {
subcards.style.display = "none";
} else {
subcards.style.display = "block";
}
}

View file

@ -1,26 +1,41 @@
use crate::config::Config;
use based::{
request::{RequestContext, StringResponse},
ui::components::Shell,
};
use maud::{html, Render};
pub fn gen_site(c: &Config) -> String {
maud::html!(
(maud::DOCTYPE)
html {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title { "Umbrella ☂️"};
link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css";
style { (maud::PreEscaped(include_str!("style.css"))) };
};
body {
script { (maud::PreEscaped(include_str!("script.js"))) };
main class="container" {
h1 { "Umbrella ☂️" };
use based::ui::prelude::*;
@for (id, card) in &c.projects {
(card.build(id));
}
};
};
}
).into_string()
use crate::config::ProjectConfig;
pub fn gen_shell() -> Shell {
Shell::new(
html! {
meta charset="UTF-8";
title { "Umbrella ☂️"};
},
Nothing(),
Background(Text("").white()).color(Colors::Black),
)
.use_ui()
}
pub async fn gen_site(c: &ProjectConfig, ctx: RequestContext) -> StringResponse {
let shell = gen_shell();
let content = Div()
.vanish()
.push(Container(
Div()
.vanish()
.push(Text("Umbrella ☂️")._3xl().bold())
.push(html! {
@for (key, _) in c {
(Link(&format!("/prj/{key}"), Text(&key)))
}
}),
))
.render();
shell.render_page(content, "Umbrella", ctx).await
}

View file

@ -1,29 +0,0 @@
.card {
border: 1px solid #ccc;
padding: 1rem;
margin: 1rem 0;
position: relative;
transition: max-height 0.3s ease-in-out;
overflow: hidden;
}
.subcards {
display: none;
margin-left: 1rem;
}
.expand-button {
position: absolute;
top: 10px;
right: 10px;
background-color: #141414;
color: white;
border: none;
padding: 0.3rem 0.6rem;
cursor: pointer;
border-radius: 0.25rem;
}
.expand-button:hover {
background-color: #0056b3;
}