parent
fd2c9457d1
commit
bf5cb72c16
10 changed files with 2214 additions and 300 deletions
2111
Cargo.lock
generated
2111
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -8,3 +8,5 @@ maud = "0.26.0"
|
||||||
rocket = "0.5.1"
|
rocket = "0.5.1"
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
serde_yml = "0.0.12"
|
serde_yml = "0.0.12"
|
||||||
|
toml = "0.8.19"
|
||||||
|
based = { git = "https://git.hydrar.de/jmarya/based", branch = "ui" }
|
||||||
|
|
29
config.toml
Normal file
29
config.toml
Normal 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"
|
13
config.yml
13
config.yml
|
@ -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"
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {}
|
||||||
pub projects: HashMap<String, Project>,
|
|
||||||
}
|
pub type ProjectConfig = HashMap<String, Project>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
|
@ -16,7 +15,7 @@ pub struct Project {
|
||||||
pub documentation: Option<String>,
|
pub documentation: Option<String>,
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
pub contact: Option<ContactInfo>,
|
pub contact: Option<ContactInfo>,
|
||||||
pub sub: Option<HashMap<String, Project>>,
|
pub sub: Option<ProjectConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -24,58 +23,9 @@ pub struct ContactInfo {
|
||||||
pub email: Option<String>,
|
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 {
|
impl Config {
|
||||||
pub fn load(path: &str) -> Self {
|
pub fn load(path: &str) -> ProjectConfig {
|
||||||
let content = std::fs::read_to_string(path).unwrap();
|
let content = std::fs::read_to_string(path).unwrap();
|
||||||
serde_yml::from_str(&content).unwrap()
|
toml::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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/invest.rs
Normal file
10
src/invest.rs
Normal 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,
|
||||||
|
}
|
187
src/main.rs
187
src/main.rs
|
@ -1,13 +1,73 @@
|
||||||
use std::path::Path;
|
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::fs::NamedFile;
|
||||||
use rocket::response::content::RawHtml;
|
use rocket::request::FromSegments;
|
||||||
use rocket::{get, launch, routes, State};
|
use rocket::{get, launch, routes, State};
|
||||||
|
use site::gen_shell;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod invest;
|
||||||
mod site;
|
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>")]
|
#[get("/static/<file>")]
|
||||||
async fn icon_res(file: &str) -> Option<NamedFile> {
|
async fn icon_res(file: &str) -> Option<NamedFile> {
|
||||||
let path = Path::new("static").join(file);
|
let path = Path::new("static").join(file);
|
||||||
|
@ -15,8 +75,119 @@ async fn icon_res(file: &str) -> Option<NamedFile> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub fn main_page(c: &State<Config>) -> RawHtml<String> {
|
pub async fn main_page(
|
||||||
RawHtml(site::gen_site(c))
|
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
|
// todo : fav icon
|
||||||
|
@ -26,10 +197,14 @@ async fn rocket() -> _ {
|
||||||
let conf_path: String = std::env::args()
|
let conf_path: String = std::env::args()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("./config.yml".to_string());
|
.unwrap_or("./config.toml".to_string());
|
||||||
let conf = config::Config::load(&conf_path);
|
let conf = config::Config::load(&conf_path);
|
||||||
|
|
||||||
|
let shell = gen_shell();
|
||||||
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![main_page, icon_res])
|
.mount_assets()
|
||||||
|
.mount("/", routes![main_page, icon_res, project_page])
|
||||||
.manage(conf)
|
.manage(conf)
|
||||||
|
.manage(shell)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
61
src/site.rs
61
src/site.rs
|
@ -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 {
|
use based::ui::prelude::*;
|
||||||
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 ☂️" };
|
|
||||||
|
|
||||||
@for (id, card) in &c.projects {
|
use crate::config::ProjectConfig;
|
||||||
(card.build(id));
|
|
||||||
}
|
pub fn gen_shell() -> Shell {
|
||||||
};
|
Shell::new(
|
||||||
};
|
html! {
|
||||||
}
|
meta charset="UTF-8";
|
||||||
).into_string()
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue