This commit is contained in:
parent
d44246a483
commit
197e4cfde9
8 changed files with 302 additions and 50 deletions
|
@ -12,6 +12,7 @@ pub struct Project {
|
|||
pub description: String,
|
||||
pub icon: Option<String>,
|
||||
pub website: Option<String>,
|
||||
pub repositories: Option<Vec<String>>,
|
||||
pub documentation: Option<String>,
|
||||
pub since: Option<String>,
|
||||
pub contact: Option<ContactInfo>,
|
||||
|
|
|
@ -1,10 +1,97 @@
|
|||
// TODO : Investment feature
|
||||
// TODO : Reoccuring investments
|
||||
|
||||
use based::get_pg;
|
||||
use rocket::{form::Form, post, response::Redirect, FromForm};
|
||||
use sqlx::prelude::FromRow;
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct Investment {
|
||||
pub id: i64,
|
||||
pub project: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub amount: f64,
|
||||
pub what: String,
|
||||
pub why: String,
|
||||
pub when: String,
|
||||
pub payed: bool,
|
||||
}
|
||||
|
||||
impl Investment {
|
||||
pub async fn new_investment(
|
||||
project: &str,
|
||||
amount: f64,
|
||||
title: &str,
|
||||
description: &str,
|
||||
) -> Investment {
|
||||
let project = project.trim_end_matches("/");
|
||||
sqlx::query_as(
|
||||
"INSERT INTO investment (project, title, description, amount)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(project)
|
||||
.bind(title)
|
||||
.bind(description)
|
||||
.bind(amount)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn pay(id: i64) -> String {
|
||||
let res: (String,) =
|
||||
sqlx::query_as("UPDATE investment SET payed = true WHERE id = $1 RETURNING project")
|
||||
.bind(id)
|
||||
.fetch_one(get_pg!())
|
||||
.await
|
||||
.unwrap();
|
||||
res.0
|
||||
}
|
||||
|
||||
pub async fn get_all_investments_of(prj: &str) -> Vec<Investment> {
|
||||
let prj = prj.trim_end_matches("/");
|
||||
sqlx::query_as("SELECT * FROM investment WHERE project = $1 ORDER BY \"when\" DESC")
|
||||
.bind(prj)
|
||||
.fetch_all(get_pg!())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_total_of(prj: &str) -> (f64, f64) {
|
||||
let prj = prj.trim_end_matches("/");
|
||||
let payed: Result<(f64,), sqlx::Error> = sqlx::query_as(
|
||||
"SELECT SUM(amount) FROM investment WHERE project = $1 AND payed = true",
|
||||
)
|
||||
.bind(prj)
|
||||
.fetch_one(get_pg!())
|
||||
.await;
|
||||
let total: Result<(f64,), sqlx::Error> =
|
||||
sqlx::query_as("SELECT SUM(amount) FROM investment WHERE project = $1")
|
||||
.bind(prj)
|
||||
.fetch_one(get_pg!())
|
||||
.await;
|
||||
if let (Ok(payed), Ok(total)) = (payed, total) {
|
||||
(payed.0, total.0)
|
||||
} else {
|
||||
(0.00, 0.00)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct NewInvestForm {
|
||||
title: String,
|
||||
amount: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[post("/prj/<root>/invest", data = "<form>")]
|
||||
pub async fn post_new_invest(root: String, form: Form<NewInvestForm>) -> Redirect {
|
||||
Investment::new_investment(
|
||||
&root,
|
||||
form.amount.parse().unwrap(),
|
||||
&form.title,
|
||||
&form.description,
|
||||
)
|
||||
.await;
|
||||
Redirect::to(format!("/prj/{}?tab=invest", root))
|
||||
}
|
||||
|
|
176
src/main.rs
176
src/main.rs
|
@ -1,17 +1,25 @@
|
|||
use std::path::Path;
|
||||
|
||||
use based::asset::AssetRoutes;
|
||||
use based::page;
|
||||
use based::request::{RequestContext, StringResponse};
|
||||
use based::ui::components::{Breadcrumb, Shell, Tabs};
|
||||
use based::ui::prelude::*;
|
||||
use based::ui::primitives::flex::Row;
|
||||
use based::ui::components::prelude::{
|
||||
ColoredMaterialIcon, MaterialIcon, Modal, ModalOpenButton, Shell, Timeline, TimelineElement,
|
||||
Tooltip,
|
||||
};
|
||||
use based::ui::components::{Breadcrumb, Card, ProgressBar, Tabs};
|
||||
use based::ui::primitives::div::Center;
|
||||
use based::ui::primitives::flex::{Column, Row};
|
||||
use based::ui::primitives::list::{CheckIcon, CheckIconRounded, CheckIconRoundedGray};
|
||||
use based::ui::primitives::Optional;
|
||||
use based::ui::{prelude::*, AttrExtendable};
|
||||
use based::{get_pg, page};
|
||||
use config::{get_prj, Project, ProjectConfig};
|
||||
use invest::Investment;
|
||||
use maud::{PreEscaped, Render};
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::request::FromSegments;
|
||||
use rocket::{get, launch, routes, State};
|
||||
use rocket::response::Redirect;
|
||||
use rocket::{get, launch, post, routes, State};
|
||||
use site::gen_shell;
|
||||
|
||||
mod config;
|
||||
|
@ -52,19 +60,20 @@ impl<'r> FromSegments<'r> for PathSegment {
|
|||
}
|
||||
}
|
||||
|
||||
#[get("/prj/<path..>")]
|
||||
#[get("/prj/<path..>?<tab>")]
|
||||
pub async fn project_page(
|
||||
path: PathSegment,
|
||||
ctx: RequestContext,
|
||||
shell: &State<Shell>,
|
||||
c: &State<ProjectConfig>,
|
||||
tab: Option<&str>,
|
||||
) -> Option<StringResponse> {
|
||||
let mut prj = c.get(path.segments.first()?);
|
||||
for p in &path.segments[1..] {
|
||||
prj = prj.as_ref()?.sub.as_ref()?.get(p);
|
||||
}
|
||||
|
||||
Some(render_project(shell, ctx, prj?, c, path.to_str()).await)
|
||||
Some(render_project(shell, ctx, prj?, c, path.to_str(), tab.unwrap_or_default()).await)
|
||||
}
|
||||
|
||||
#[get("/static/<file>")]
|
||||
|
@ -84,7 +93,7 @@ pub async fn main_page(
|
|||
let first = keys.first().unwrap();
|
||||
let prj = c.get(*first).unwrap();
|
||||
|
||||
render_project(shell, ctx, prj, c, format!("{first}/")).await
|
||||
render_project(shell, ctx, prj, c, format!("{first}/"), "").await
|
||||
} else {
|
||||
// TODO : root project overview
|
||||
site::gen_site(c, ctx).await
|
||||
|
@ -105,6 +114,120 @@ pub fn DetailLink(text: &str, reference: &str) -> PreEscaped<String> {
|
|||
.render()
|
||||
}
|
||||
|
||||
pub fn build_invest_timeline(investments: &[Investment]) -> PreEscaped<String> {
|
||||
let mut t = Timeline();
|
||||
|
||||
for i in investments {
|
||||
t = t.add_element(TimelineElement::new(
|
||||
Text(&i.when),
|
||||
Row(vec![
|
||||
Text(&i.title).render(),
|
||||
Text(&format!("{:.2} $", i.amount)).render(),
|
||||
if i.payed {
|
||||
Tooltip(CheckIconRounded(), Text("Payed")).render()
|
||||
} else {
|
||||
CheckIconRoundedGray()
|
||||
},
|
||||
])
|
||||
.gap(ScreenValue::_2)
|
||||
.items_center(),
|
||||
Column(vec![
|
||||
Text(&i.description).render(),
|
||||
if !i.payed {
|
||||
Form::new(&format!("/invest/pay?id={}", i.id))
|
||||
.add_input(FormSubmitButton(Text("Pay")))
|
||||
.render()
|
||||
} else {
|
||||
Nothing()
|
||||
},
|
||||
])
|
||||
.gap(ScreenValue::_2),
|
||||
))
|
||||
}
|
||||
|
||||
t.render()
|
||||
}
|
||||
|
||||
pub async fn invest_tab(prj: &Project, root: &str) -> PreEscaped<String> {
|
||||
let (add_invest_id, add_invest_modal) = Modal(
|
||||
"Add Investment",
|
||||
Form::new(&format!("/prj/{root}invest"))
|
||||
.id("new_invest")
|
||||
.add_input(TextInput("title").required().placeholder("Title"))
|
||||
.add_input(
|
||||
TextInput("amount")
|
||||
.icon(MaterialIcon("attach_money"))
|
||||
.required()
|
||||
.placeholder("Amount")
|
||||
.pattern(r#"^\d+(\.\d+)?$"#),
|
||||
)
|
||||
.add_input(
|
||||
TextArea()
|
||||
.name("description")
|
||||
.required()
|
||||
.placeholder("Description"),
|
||||
),
|
||||
|_| {
|
||||
Center(Context(
|
||||
FormSubmitButton("Create").add_attr("form", "new_invest"),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let investments = Investment::get_all_investments_of(root).await;
|
||||
|
||||
let t = build_invest_timeline(&investments);
|
||||
|
||||
let inv = Investment::get_total_of(root).await;
|
||||
|
||||
let percentage = inv.0 / inv.1;
|
||||
let percentage = (percentage * 100.0) as u8;
|
||||
|
||||
Column(vec![
|
||||
add_invest_modal,
|
||||
Margin(
|
||||
Row(vec![
|
||||
Margin(Width(
|
||||
ScreenValue::full,
|
||||
Row(vec![
|
||||
ColoredMaterialIcon("paid", Green::_600),
|
||||
Text(&format!(
|
||||
"Total Investment: {:.2} $ / {:.2} $",
|
||||
inv.0, inv.1
|
||||
))
|
||||
.color(&Green::_600)
|
||||
.render(),
|
||||
Width(
|
||||
ScreenValue::full,
|
||||
Margin(ProgressBar(percentage, true, Green::_600)).x(ScreenValue::_12),
|
||||
)
|
||||
.render(),
|
||||
])
|
||||
.gap(ScreenValue::_2)
|
||||
.full_center(),
|
||||
))
|
||||
.x(ScreenValue::auto)
|
||||
.render(),
|
||||
ModalOpenButton(
|
||||
&add_invest_id,
|
||||
Padding(
|
||||
Rounded(Background(MaterialIcon("add")).color(Blue::_600))
|
||||
.size(Size::Medium),
|
||||
)
|
||||
.x(ScreenValue::_2)
|
||||
.top(ScreenValue::_2),
|
||||
)
|
||||
.render(),
|
||||
])
|
||||
.justify(Justify::Between),
|
||||
)
|
||||
.left(ScreenValue::_4)
|
||||
.render(),
|
||||
t,
|
||||
])
|
||||
.render()
|
||||
}
|
||||
|
||||
pub fn info_tab(prj: &Project, root: &str) -> PreEscaped<String> {
|
||||
Flex(
|
||||
Div()
|
||||
|
@ -119,6 +242,14 @@ pub fn info_tab(prj: &Project, root: &str) -> PreEscaped<String> {
|
|||
.push(Optional(prj.documentation.as_deref(), |docs| {
|
||||
DetailLink("Documentation:", docs)
|
||||
}))
|
||||
.push(Optional(prj.repositories.as_deref(), |repos| {
|
||||
Column(vec![
|
||||
Text("Repositories:").xl().semibold().render(),
|
||||
UnorderedList()
|
||||
.push_for_each(repos, |repo: &_| Link(repo, Text(repo)))
|
||||
.render(),
|
||||
])
|
||||
}))
|
||||
.push(Optional(prj.since.as_deref(), |since| {
|
||||
Text(&format!("Since: {since}"))
|
||||
}))
|
||||
|
@ -193,6 +324,7 @@ pub async fn render_project(
|
|||
prj: &Project,
|
||||
c: &State<ProjectConfig>,
|
||||
mut root: String,
|
||||
tab: &str,
|
||||
) -> StringResponse {
|
||||
let title = format!("{} - Umbrella ☂️", prj.name);
|
||||
|
||||
|
@ -214,6 +346,8 @@ pub async fn render_project(
|
|||
})
|
||||
.collect();
|
||||
|
||||
let invest_tab = invest_tab(prj, &root).await;
|
||||
|
||||
page!(
|
||||
shell,
|
||||
ctx,
|
||||
|
@ -230,12 +364,9 @@ pub async fn render_project(
|
|||
.push(
|
||||
Padding(
|
||||
Tabs()
|
||||
.active(tab)
|
||||
.add_tab("info", Text("📜 Info").medium(), info_tab(prj, &root))
|
||||
.add_tab(
|
||||
"invest",
|
||||
Text("💵 Invest").medium(),
|
||||
Div().vanish().push(Text("TODO"))
|
||||
)
|
||||
.add_tab("invest", Text("💵 Invest").medium(), invest_tab)
|
||||
)
|
||||
.x(ScreenValue::_6)
|
||||
)
|
||||
|
@ -254,9 +385,26 @@ async fn rocket() -> _ {
|
|||
|
||||
let shell = gen_shell();
|
||||
|
||||
sqlx::migrate!("./migrations").run(get_pg!()).await.unwrap();
|
||||
|
||||
rocket::build()
|
||||
.mount_assets()
|
||||
.mount("/", routes![main_page, icon_res, project_page])
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
main_page,
|
||||
icon_res,
|
||||
project_page,
|
||||
invest::post_new_invest,
|
||||
pay_investment
|
||||
],
|
||||
)
|
||||
.manage(conf)
|
||||
.manage(shell)
|
||||
}
|
||||
|
||||
#[post("/invest/pay?<id>")]
|
||||
pub async fn pay_investment(id: i64) -> Redirect {
|
||||
let prj = Investment::pay(id).await;
|
||||
Redirect::to(format!("/prj/{}?tab=invest", prj))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use based::{
|
||||
request::{RequestContext, StringResponse},
|
||||
ui::components::Shell,
|
||||
ui::components::prelude::Shell,
|
||||
};
|
||||
use maud::{html, Render};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue