diff --git a/Cargo.lock b/Cargo.lock index 8513f44..7d0c4d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,9 +110,9 @@ dependencies = [ "axum", "axum-extra", "clap", + "el", "fontdb 0.23.0", "jiff", - "maud", "serde", "tokio", "typst", @@ -610,6 +610,16 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "el" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d62f2117df9caa2f568124894462af4ba7dfd9a2ab8b82b79c29ce3b0c16eb" +dependencies = [ + "axum-core", + "http", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -1307,30 +1317,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "maud" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" -dependencies = [ - "axum-core", - "http", - "itoa", - "maud_macros", -] - -[[package]] -name = "maud_macros" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1658,29 +1644,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.92" diff --git a/Cargo.toml b/Cargo.toml index f31169e..17539d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,9 @@ anyhow = "1.0.94" axum = "0.7.9" axum-extra = { version = "0.9.6", features = ["form"] } clap = { version = "4.5.23", features = ["derive", "deprecated"] } +el = { version = "0.1.2", features = ["axum"] } fontdb = "0.23.0" jiff = "0.1.15" -maud = { version = "0.26.0", features = ["axum"] } serde = { version = "1.0.216", features = ["derive"] } tokio = { version = "1.42.0", features = ["full"] } typst = "0.12.0" diff --git a/src/endpoints.rs b/src/endpoints.rs index 9f3d633..34c68f9 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -1,19 +1,19 @@ -use maud::{html, Markup}; +use el::{html::*, Document, ElementComponent}; pub mod index; pub mod tsg; -fn page(head: Markup, body: Markup) -> Markup { - html! { - (maud::DOCTYPE) - html lang="en" { - head { - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - title { "AbzDokGen" } - (head) - } - body { (body) } - } - } +fn page(head: impl ElementComponent, body: impl ElementComponent) -> Document { + html(( + el::html::head(( + meta(( + attr::name("viewport"), + attr::content("width=device-width, initial-scale=1"), + )), + title("AbzDokGen"), + head, + )), + el::html::body(body), + )) + .into_document() } diff --git a/src/endpoints/index.rs b/src/endpoints/index.rs index 90f3492..a67dc59 100644 --- a/src/endpoints/index.rs +++ b/src/endpoints/index.rs @@ -1,10 +1,12 @@ +use std::iter; + use axum::{ http::{header, StatusCode}, response::{IntoResponse, Response}, }; use axum_extra::extract::Form; +use el::{html::*, Document}; use jiff::{ToSpan, Zoned}; -use maud::{html, Markup, PreEscaped}; use serde::Deserialize; use crate::{ @@ -12,7 +14,12 @@ use crate::{ render::{self, Entry, Note, Timesheet, WorkingArea}, }; -pub async fn get() -> Markup { +const LINK_SOURCE: &str = "https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator"; +const LINK_TSG: &str = "https://github.com/kit-sdq/TimeSheetGenerator"; +const LINK_TEMPLATE: &str = + "https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator/blob/master/kit_timesheet.md"; + +pub async fn get() -> Document { // We assume that people still want to fill out the previous month's time // sheet during the first two weeks of the following month. let month = Zoned::now() @@ -20,117 +27,252 @@ pub async fn get() -> Markup { .unwrap() .strftime("%Y-%m"); - page( - html! { - style { (PreEscaped(include_str!("index.css"))) } - script type="module" { (PreEscaped(include_str!("index.js"))) } - }, - html! { - form #form { - h1 { - "Arbeitszeitdokumentationsgenerator " - a #source href="https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator" { "(source)" } - } + let head = ( + style(include_str!("index.css")), + script((attr::TypeScript::Module, include_str!("index.js"))), + ); - p { - "Du kannst auch " - a href="tsg/" { "JSON eingeben" } - ", das kompatibel mit dem " - a href="https://github.com/kit-sdq/TimeSheetGenerator" { "TimeSheetGenerator" } - " ist, oder das dem Generator zugrunde liegende " - a href="https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator/blob/master/kit_timesheet.md" { "Typst-Template" } - " direkt benutzen." - } + let body = form(( + attr::id("form"), + h1(( + "Arbeitszeitdokumentationsgenerator ", + a((attr::id("source"), attr::href(LINK_SOURCE), "(source)")), + )), + p(( + "Du kannst auch ", + a((attr::href("tsg/"), "JSON eingeben")), + ", das kompatibel mit dem ", + a((attr::href(LINK_TSG), "TimeSheetGenerator")), + " ist, oder das dem Generator zugrunde liegende ", + a((attr::href(LINK_TEMPLATE), "Typst-Template")), + " direkt benutzen.", + )), + div(( + attr::id("header"), + label((attr::id("l-month"), attr::r#for("i-month"), "Monat / Jahr:")), + input(( + attr::id("i-month"), + attr::name("month"), + attr::TypeInput::Month, + attr::placeholder(&month), + attr::value(&month), + )), + label(( + attr::id("l-name"), + attr::r#for("i-name"), + "Name, Vorname des/r Beschäftigten:", + )), + input(( + attr::id("i-name"), + attr::class("twocol"), + attr::name("name"), + attr::TypeInput::Text, + attr::placeholder("McStudentface, Student"), + )), + label(( + attr::id("l-staffid"), + attr::r#for("i-staffid"), + "Personalnummer:", + )), + input(( + attr::id("i-staffid"), + attr::name("staff_id"), + attr::TypeInput::Text, + attr::placeholder("1337420"), + )), + div(( + attr::id("gfub"), + label(( + attr::id("l-gf"), + attr::title("Großforschung"), + "GF: ", + input(( + attr::id("i-gf"), + attr::name("working_area"), + attr::TypeInput::Radio, + attr::value("GF"), + )), + )), + label(( + attr::id("l-ub"), + attr::title("Unibereich"), + "UB: ", + input(( + attr::id("i-ub"), + attr::name("working_area"), + attr::TypeInput::Radio, + attr::value("UB"), + attr::checked(), + )), + )), + )), + label(( + attr::id("l-department"), + attr::r#for("i-department"), + attr::title("Institut/Organisationseinheit"), + "OE:", + )), + input(( + attr::id("i-department"), + attr::class("twocol"), + attr::name("department"), + attr::TypeInput::Text, + attr::placeholder("Institut für Informatik"), + attr::value("Institut für Informatik"), + )), + label(( + attr::id("l-monthlyhours"), + attr::r#for("i-monthlyhours"), + "Vertraglich vereinbarte Arbeitszeit:", + )), + div(( + attr::id("mhhr"), + attr::class("twocol"), + span(( + input(( + attr::id("i-monthlyhours"), + attr::name("monthly_hours"), + attr::TypeInput::Number, + attr::value(40), + attr::min(0), + )), + " Std.", + )), + span(( + label(( + attr::id("l-hourlywage"), + attr::r#for("i-hourlywage"), + "Stundensatz: ", + )), + input(( + attr::id("i-hourlywage"), + attr::name("hourly_wage"), + attr::TypeInput::Number, + attr::step(0.01), + attr::value(14.09), + )), + " €", + )), + )), + div(( + attr::id("carry"), + attr::class("twocol"), + span(( + label(( + attr::id("l-carry"), + attr::r#for("i-carry"), + "Übertrag vom Vormonat: ", + )), + input(( + attr::id("i-carry"), + attr::class("i-dur"), + attr::name("carry_prev_month"), + attr::TypeInput::Text, + attr::placeholder("00:00"), + )), + )), + )), + label(( + attr::id("check"), + attr::title(concat!( + "Die Tabelleneinträge werden chronologisch sortiert,", + " anstatt dass ihre Reihenfolge beibehalten wird." + )), + "Einträge sortieren ", + input(( + attr::name("sort"), + attr::TypeInput::Checkbox, + attr::value(true), + attr::checked(), + )), + )), + label(( + attr::id("validate"), + attr::title(concat!( + "Die Tabelleneinträge werden auf Konsistenz und Korrektheit überprüft,", + " bevor das Dokument generiert wird." + )), + "Einträge validieren ", + input(( + attr::name("validate"), + attr::TypeInput::Checkbox, + attr::value(true), + attr::checked(), + )), + )), + )), + div(( + attr::id("table"), + div(( + attr::id("task"), + "Tätigkeit", + br(()), + "(Stichwort, Projekt)", + )), + div("Tag"), + div("Beginn"), + div("Ende"), + div("Pause"), + div("Arbeitszeit"), + div(()), + div("(hh:mm)"), + div("(hh:mm)"), + div("(hh:mm)"), + div(()), + iter::repeat(( + div(input(( + attr::class("i-task"), + attr::name("task"), + attr::TypeInput::Text, + ))), + div(input(( + attr::class("i-day"), + attr::name("day"), + attr::TypeInput::Number, + attr::placeholder(1), + attr::min(1), + attr::max(31), + ))), + div(input(( + attr::class("i-dur"), + attr::name("start"), + attr::TypeInput::Text, + attr::placeholder("12:34"), + ))), + div(input(( + attr::class("i-dur"), + attr::name("end"), + attr::TypeInput::Text, + attr::placeholder("12:34"), + ))), + div(input(( + attr::class("i-dur"), + attr::name("rest"), + attr::TypeInput::Text, + attr::placeholder("00:00"), + ))), + div(select(( + attr::name("note"), + attr::value(""), + option((attr::value(""), "Normal")), + option((attr::value("U"), "Urlaub")), + option((attr::value("K"), "Krankheit")), + option((attr::value("F"), "Feiertag")), + option((attr::value("S"), "Sonstiges")), + ))), + )) + .take(22) + .collect::>(), + )), + button(( + attr::id("submit"), + attr::TypeButton::Button, + "Arbeitszeitdokumentation generieren", + )), + pre(attr::id("info")), + )); - div #header { - label #l-month for="i-month" { "Monat / Jahr:" } - input #i-month name="month" type="month" placeholder=(month) value=(month) {} - - label #l-name for="i-name" { "Name, Vorname des/r Beschäftigten:" } - input #i-name .twocol name="name" type="text" placeholder="McStudentface, Student" {} - - label #l-staffid for="i-staffid" { "Personalnummer:" } - input #i-staffid name="staff_id" type="text" placeholder="1337420" {} - - div #gfub { - label #l-gf title="Großforschung" { "GF: " - input #i-gf name="working_area" type="radio" value="GF" {} - } - - label #l-ub for="i-ub" title="Unibereich" { "UB: " - input #i-ub name="working_area" type="radio" value="UB" checked {} - } - } - - label #l-department for="i-department" title="Organisationseinheit" { "OE:" } - input #i-department .twocol name="department" type="text" placeholder="Institut für Informatik" value="Institut für Informatik" {} - - label #l-monthlyhours for="i-monthlyhours" { "Vertraglich vereinbarte Arbeitszeit:" } - div #mhhr .twocol { - span { - input #i-monthlyhours name="monthly_hours" type="number" value="40" min="0" {} - " Std." - } - span { - label #l-hourlywage for="i-hourlywage" { "Stundensatz: " } - input #i-hourlywage name="hourly_wage" type="number" step="0.01" value="14.09" {} - " €" - } - } - - div #carry .twocol { - span { - label #l-carry for="i-carry" { "Übertrag vom Vormonat: " } - input #i-carry .i-dur name="carry_prev_month" type="text" placeholder="00:00" {} - } - } - - label #check title="Die Tabelleneinträge werden chronologisch sortiert, anstatt dass ihre Reihenfolge beibehalten wird." { - "Einträge sortieren " - input name="sort" type="checkbox" value="true" checked {} - } - - label #validate title="Die Tabelleneinträge werden auf Konsistenz und Korrektheit überprüft, bevor das Dokument generiert wird." { - "Einträge validieren " - input name="validate" type="checkbox" value="true" checked {} - } - } - - div #table { - div #task { "Tätigkeit" br; "(Stichwort, Projekt)" } - div { "Tag" } - div { "Beginn" } - div { "Ende" } - div { "Pause" } - div { "Arbeitszeit" } - div { } - div { "(hh:mm)" } - div { "(hh:mm)" } - div { "(hh:mm)" } - div { } - - @for _ in 0..22 { - div { input .i-task name="task" type="text" {} } - div { input .i-day name="day" type="number" placeholder="1" min="1" max="31" {} } - div { input .i-dur name="start" type="text" placeholder="12:34" {} } - div { input .i-dur name="end" type="text" placeholder="12:34" {} } - div { input .i-dur name="rest" type="text" placeholder="00:00" {} } - div { select name="note" value="" { - option value="" { "Normal" } - option value="U" { "Urlaub" } - option value="K" { "Krankheit" } - option value="F" { "Feiertag" } - option value="S" { "Sonstiges" } - } } - } - } - - button #submit type="button" { "Arbeitszeitdokumentation generieren" } - - pre #info {} - } - }, - ) + page(head, body) } #[derive(Debug, Deserialize)] diff --git a/src/endpoints/tsg.rs b/src/endpoints/tsg.rs index 3eb4afe..15af1a8 100644 --- a/src/endpoints/tsg.rs +++ b/src/endpoints/tsg.rs @@ -3,7 +3,7 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use maud::{html, Markup, PreEscaped}; +use el::{html::*, Document}; use serde::Deserialize; use crate::{ @@ -11,53 +11,76 @@ use crate::{ render::{self, Entry, Note, Timesheet, WorkingArea}, }; -pub async fn get() -> Markup { - page( - html! { - style { (PreEscaped(include_str!("tsg.css"))) } - script type="module" { (PreEscaped(include_str!("tsg.js"))) } - }, - html! { - form #form { - h1 { - "Arbeitszeitdokumentationsgenerator " - a #source href="https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator" { "(source)" } - } +const LINK_SOURCE: &str = "https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator"; - p { - "Du kannst deine Daten auch in einem " - a href=".." { "coolen Formular" } - " eingeben." - } +pub async fn get() -> Document { + let head = ( + style(include_str!("tsg.css")), + script((attr::TypeScript::Module, include_str!("tsg.js"))), + ); - p { - label for="i-global" { "Global.json" } - textarea #i-global name="global" placeholder="{}" {} - } + let body = form(( + attr::id("form"), + h1(( + "Arbeitszeitdokumentationsgenerator ", + a((attr::id("source"), attr::href(LINK_SOURCE), "(source)")), + )), + p(( + "Du kannst deine Daten auch in einem ", + a((attr::href(".."), "coolen Formular")), + " eingeben.", + )), + p(( + label((attr::r#for("i-global"), "Global.json")), + textarea(( + attr::id("i-global"), + attr::name("global"), + attr::placeholder("{}"), + )), + )), + p(( + label((attr::r#for("i-month"), "Month.json")), + textarea(( + attr::id("i-month"), + attr::name("month"), + attr::placeholder("{}"), + )), + )), + p(( + label(( + attr::title(concat!( + "Die Einträge werden chronologisch sortiert,", + " anstatt dass ihre Reihenfolge beibehalten wird." + )), + input(( + attr::name("sort"), + attr::TypeInput::Checkbox, + attr::checked(), + )), + " Einträge sortieren", + )), + label(( + attr::title(concat!( + "Die Einträge werden auf Konsistenz und Korrektheit überprüft,", + " bevor das Dokument generiert wird." + )), + input(( + attr::name("validate"), + attr::TypeInput::Checkbox, + attr::checked(), + )), + " Einträge validieren", + )), + )), + button(( + attr::id("submit"), + attr::TypeButton::Button, + "Arbeitszeitdokumentation generieren", + )), + pre(attr::id("info")), + )); - p { - label for="i-month" { "Month.json" } - textarea #i-month name="month" placeholder="{}" {} - } - - p { - label title="Die Einträge werden chronologisch sortiert, anstatt dass ihre Reihenfolge beibehalten wird." { - input name="sort" type="checkbox" checked {} - " Einträge sortieren" - } - - label title="Die Einträge werden auf Konsistenz und Korrektheit überprüft, bevor das Dokument generiert wird." { - input name="validate" type="checkbox" checked {} - " Einträge validieren" - } - } - - button #submit type="button" { "Arbeitszeitdokumentation generieren" } - - pre #info {} - } - }, - ) + page(head, body) } fn default_vacation() -> bool {