diff options
| author | WriterV2 <primary-v2@protonmail.com> | 2024-11-01 16:09:20 +0100 |
|---|---|---|
| committer | WriterV2 <primary-v2@protonmail.com> | 2024-11-01 16:09:20 +0100 |
| commit | 0d6cc15cc846e4266fb34854f8254b91ad1e6111 (patch) | |
| tree | a2a0ab4bc35e2461a1d7dbf228ba226442fce6fb /src/handlers | |
| parent | 6092e37e3a8e890cbe9d13305bcaf67daf410d93 (diff) | |
Feat: Add Frontend builder
- update color scheme (TODO: accent color not final)
- add product frontend helper to build card
- add product frontend helper to build cards layout
- add productmarker and implement for story, product and tool
- add container for product and specific product (TODO: Refactoring)
- add page builder for products
- add temporary dummy product (TODO: Remove after first real
implementation)
Diffstat (limited to 'src/handlers')
| -rw-r--r-- | src/handlers/frontend.rs | 37 | ||||
| -rw-r--r-- | src/handlers/frontend_builder.rs | 189 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 7 |
3 files changed, 194 insertions, 39 deletions
diff --git a/src/handlers/frontend.rs b/src/handlers/frontend.rs deleted file mode 100644 index f4b7c6c..0000000 --- a/src/handlers/frontend.rs +++ /dev/null @@ -1,37 +0,0 @@ -use axum::response::IntoResponse; -use maud::{html, Markup, DOCTYPE}; - -use crate::error::AppError; - -pub async fn home() -> Result<impl IntoResponse, AppError> { - Ok(html!({ - (head("Home")) - body class="bg-[#eae3d2] text-[#333] dark:bg-[#333] dark:text-[#eae3d2] font-mono text-base tracking-wide" { - (header_with_navbar()) - } - })) -} - -fn header_with_navbar() -> Markup { - html!({ - header { - navbar class="p-3 text-sm flex flex-row flex-wrap content-center justify-around decoration-purple-500 shadow" { - a class="hover:underline" href="/" { "Home" } - a class="hover:underline" href="/" { "Stories" } - a class="hover:underline" href="/" { "Tools" } - a class="hover:underline" href="/" { "Games" } - } - } - }) -} - -fn head(page_title: &str) -> Markup { - html! { - head { - (DOCTYPE) - meta charset="utf-8"; - link rel="stylesheet" type="text/css" href="/static/styles.css"; - title { (page_title) } - } - } -} diff --git a/src/handlers/frontend_builder.rs b/src/handlers/frontend_builder.rs new file mode 100644 index 0000000..8e3a9f2 --- /dev/null +++ b/src/handlers/frontend_builder.rs @@ -0,0 +1,189 @@ +use axum::response::IntoResponse; +use maud::{html, Markup, DOCTYPE}; + +use crate::db::product::{Product, ProductMarker}; +use crate::error::AppError; + +pub async fn home() -> Result<impl IntoResponse, AppError> { + Ok(html!((page("Home".to_string(), Markup::default())))) +} + +fn page(page_title: String, content: Markup) -> Markup { + html!( + (head(page_title)) + body class="bg-[#eae3d2] text-[#333] dark:bg-[#333] dark:text-[#eae3d2] font-mono text-base" { + (header()) + (navbar()) + (content) + } + ) +} + +fn header() -> Markup { + html!( + header { + h1 class="underline decoration-[#601B69] dark:decoration-[#e1b1e7] text-center tracking-widest font-serif p-3 text-xl" {"Vithuran Vishnuthas"} + } + ) +} + +fn navbar() -> Markup { + html!({ + navbar class="p-3 text-sm flex flex-row flex-wrap content-center justify-around decoration-[#eae3d2] tracking-wide" { + a class="hover:underline" href="/" { "Home" } + a class="hover:underline" href="/stories" { "Stories" } + a class="hover:underline" href="/" { "Tools" } + a class="hover:underline" href="/" { "Games" } + } + }) +} + +fn cards<T: PageBuilder + ProductMarker>( + products: Vec<SpecificProduct<T>>, + category: String, +) -> Markup { + html!( + div class="m-12" { + h1 class="p-6 font-serif text-xl tracking-wider underline decoration-[#601B69] dark:decoration-[#E1B1E7]" { (category) } + div class="grid grid-cols-2 gap-8" { + @for product in products { + (card(product)) + } + } + } + ) +} + +fn card<T: PageBuilder + ProductMarker>(specific_product: SpecificProduct<T>) -> Markup { + html!( + div class="p-9 shadow" { + h2 class="text-[#601B69] dark:text-[#E1B1E7]" { + (specific_product.product.name) + } + div { + (specific_product.product.description) + } + div { + (specific_product.specific.product_specific_card_content()) + } + div class="flex flex-row space-x-5 mt-9 text-xs" { + span {"Update: " (specific_product.product.updatedate)} + span {"Upload: " (specific_product.product.uploaddate)} + } + } + ) +} + +fn head(page_title: String) -> Markup { + html! { + head { + (DOCTYPE) + meta charset="utf-8"; + link rel="stylesheet" type="text/css" href="/static/styles.css"; + title { (page_title) } + } + } +} + +pub async fn build_page<T: Sized + ProductMarker + PageBuilder>( +) -> Result<impl IntoResponse, AppError> { + Ok(html!( + (page(T::page_title(), cards(T::products(), T::page_title()))) + )) +} + +pub trait PageBuilder { + fn page_title() -> String; + fn products() -> Vec<SpecificProduct<Self>> + where + Self: Sized + ProductMarker; + fn product_specific_card_content(&self) -> Markup; +} + +// TODO: Remove after first implementation +#[derive(Hash, PartialEq, Eq)] +pub struct DummyProduct { + dummy: String, +} + +impl PageBuilder for DummyProduct { + fn page_title() -> String { + "Dummy Products".to_string() + } + + fn products() -> Vec<SpecificProduct<Self>> + where + Self: Sized, + { + let product1 = Product { + id: 0, + name: "Willensfreiheit in Ketten".to_string(), + description: + "Nach der Flucht seines Bruders, verzweifelt Fahim an der Existenz der Willensfreiheit." + .to_string(), + uploaddate: 1, + updatedate: 1, + }; + + let product2 = Product { + id: 1, + name: "Aufbruch und Abbruch".to_string(), + description: "Ein frischer Absolvent muss sich vor der ersten Quest nur noch bei der Gilde für Heilkünstler anmelden.".to_string(), + uploaddate: 1, + updatedate: 1, + }; + + let product3 = Product { + id: 2, + name: "Mit gutem Gewissen".to_string(), + description: "Ein frischer Absolvent muss sich vor der ersten Quest nur noch bei der Gilde für Heilkünstler anmelden.".to_string(), + uploaddate: 1, + updatedate: 1, + }; + + let test1 = DummyProduct { + dummy: "/".to_string(), + }; + + let test2 = DummyProduct { + dummy: "/".to_string(), + }; + + let test3 = DummyProduct { + dummy: "/".to_string(), + }; + + let specific1 = SpecificProduct { + product: product1, + specific: test1, + }; + + let specific2 = SpecificProduct { + product: product2, + specific: test2, + }; + + let specific3 = SpecificProduct { + product: product3, + specific: test3, + }; + + vec![specific1, specific2, specific3] + } + + fn product_specific_card_content(&self) -> Markup { + html!( + a class="mt-9 text-[#601B69] dark:text-[#E1B1E7]" href=(self.dummy) { + "Link" + } + ) + } +} + +impl ProductMarker for DummyProduct {} + +// TODO: Refactoring +pub struct SpecificProduct<T: ProductMarker> { + product: Product, + specific: T, +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d346512..9d5aa54 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -7,10 +7,12 @@ use tower_http::add_extension::AddExtensionLayer; use tower_http::services::ServeDir; use admin::{admin_healthcheck, admin_middleware, upload_story}; -use frontend::home; +use frontend_builder::home; + +use self::frontend_builder::{build_page, DummyProduct}; mod admin; -mod frontend; +mod frontend_builder; #[derive(Debug, Clone)] struct ApiContext { @@ -23,6 +25,7 @@ pub async fn router(pool: SqlitePool) -> Router { .route("/admin", get(admin_healthcheck)) .layer(middleware::from_fn(admin_middleware)) .route("/", get(home)) + .route("/stories", get(build_page::<DummyProduct>)) .nest_service("/static", ServeDir::new("src/static")) .layer(DefaultBodyLimit::max(1074000000)) .layer(AddExtensionLayer::new(ApiContext { pool })) |
