Render individual product pages
This commit is contained in:
parent
1a1a5a3451
commit
9615f13199
|
@ -8,7 +8,7 @@ use crate::authenticate::*;
|
|||
use crate::database::*;
|
||||
use crate::structures::*;
|
||||
|
||||
use crate::html::render_pages;
|
||||
use crate::html::{render_product, render_products};
|
||||
use argon2::{password_hash::SaltString, Argon2};
|
||||
|
||||
// Endpoint to add a new product
|
||||
|
@ -38,7 +38,8 @@ pub fn new_product(argon2: &State<Argon2>, request: Form<ProductRequest>) -> Jso
|
|||
}
|
||||
}
|
||||
|
||||
render_pages();
|
||||
render_products();
|
||||
render_product(new_product);
|
||||
|
||||
return Json(Status {
|
||||
// Return a JSON status
|
||||
|
@ -254,6 +255,8 @@ pub fn update_product(argon2: &State<Argon2>, update: Form<Update<'_>>) -> Json<
|
|||
}
|
||||
}
|
||||
|
||||
render_product(new_product);
|
||||
|
||||
return Json(Status {
|
||||
// Return a json success status
|
||||
status: "success".to_string(),
|
||||
|
@ -283,6 +286,8 @@ pub fn update_product(argon2: &State<Argon2>, update: Form<Update<'_>>) -> Json<
|
|||
}
|
||||
}
|
||||
|
||||
render_product(new_product);
|
||||
|
||||
return Json(Status {
|
||||
status: "success".to_string(),
|
||||
reason: "stock changed".to_string(),
|
||||
|
|
94
src/html.rs
94
src/html.rs
|
@ -7,6 +7,24 @@ use std::sync::Mutex;
|
|||
use tera::{Context, Tera};
|
||||
use toml::Value;
|
||||
|
||||
// Struct for data needed for rendering a product
|
||||
struct ProductRenderObject {
|
||||
url: String,
|
||||
tera: Tera,
|
||||
product: Product,
|
||||
}
|
||||
// Struct for data needed for rendering
|
||||
struct RenderObject<'a> {
|
||||
url: String,
|
||||
tera: Tera,
|
||||
subtitle: &'a str,
|
||||
active_page: &'a str,
|
||||
title: &'a str,
|
||||
description: &'a str,
|
||||
products: Option<Vec<Product>>,
|
||||
}
|
||||
|
||||
// Navigation bar info
|
||||
#[derive(Serialize)]
|
||||
struct NavLink {
|
||||
href: String,
|
||||
|
@ -14,6 +32,7 @@ struct NavLink {
|
|||
active: AtomicBool,
|
||||
}
|
||||
|
||||
// TOML config file
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Config {
|
||||
title: String,
|
||||
|
@ -24,6 +43,7 @@ struct Config {
|
|||
products: Products,
|
||||
}
|
||||
|
||||
// Products field in TOML
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Products {
|
||||
label: String,
|
||||
|
@ -34,6 +54,7 @@ struct Products {
|
|||
progestogens: Page,
|
||||
}
|
||||
|
||||
// TOML page info
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Page {
|
||||
label: String,
|
||||
|
@ -41,6 +62,7 @@ struct Page {
|
|||
title: String,
|
||||
}
|
||||
|
||||
// Base info for rendering a page
|
||||
struct PageBase {
|
||||
config: Config,
|
||||
tera: Tera,
|
||||
|
@ -48,6 +70,7 @@ struct PageBase {
|
|||
product_classes: Vec<NavLink>,
|
||||
}
|
||||
|
||||
// Build the base info
|
||||
fn build_base() -> PageBase {
|
||||
// Read in TOML
|
||||
let mut config_file = std::fs::File::open("config.toml").unwrap();
|
||||
|
@ -89,6 +112,7 @@ fn build_base() -> PageBase {
|
|||
|
||||
let nav_elements = vec![nav_home, nav_about, nav_products, nav_contact];
|
||||
|
||||
// Construct the product classes
|
||||
let estrogens = NavLink {
|
||||
href: config.products.estrogens.href.clone(),
|
||||
text: config.products.estrogens.label.clone(),
|
||||
|
@ -113,6 +137,8 @@ fn build_base() -> PageBase {
|
|||
product_classes,
|
||||
};
|
||||
}
|
||||
|
||||
// Render all the pages
|
||||
pub fn render_pages() {
|
||||
let base = build_base();
|
||||
// Render pages
|
||||
|
@ -164,6 +190,12 @@ pub fn render_pages() {
|
|||
&base.product_classes,
|
||||
);
|
||||
|
||||
render_products();
|
||||
}
|
||||
|
||||
// Render the products page
|
||||
pub fn render_products() {
|
||||
let base = build_base();
|
||||
let products_vec: Option<Vec<Product>>;
|
||||
match read_all_products() {
|
||||
Ok(prods) => products_vec = Some(prods),
|
||||
|
@ -186,14 +218,56 @@ pub fn render_pages() {
|
|||
);
|
||||
}
|
||||
|
||||
struct RenderObject<'a> {
|
||||
url: String,
|
||||
tera: Tera,
|
||||
subtitle: &'a str,
|
||||
active_page: &'a str,
|
||||
title: &'a str,
|
||||
description: &'a str,
|
||||
products: Option<Vec<Product>>,
|
||||
// Render a specific product page
|
||||
pub fn render_product(product: Product) {
|
||||
let base = build_base();
|
||||
let obj = ProductRenderObject {
|
||||
url: format!("products/{}/index.html", product.short_name),
|
||||
tera: base.tera.clone(),
|
||||
product,
|
||||
};
|
||||
|
||||
let url = format!("static/{}", &obj.url);
|
||||
let url_str = url.as_str();
|
||||
let prefix = std::path::Path::new(url_str).parent().unwrap();
|
||||
std::fs::create_dir_all(prefix).unwrap();
|
||||
let mut output_file = std::fs::File::create(url).expect("create failed");
|
||||
|
||||
// Change the active NavLink
|
||||
for i in &base.nav_elements {
|
||||
if i.text == "Products" {
|
||||
i.active.store(true, Ordering::Relaxed);
|
||||
} else {
|
||||
i.active.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the context
|
||||
let mut context = Context::new();
|
||||
context.insert("title", &base.config.title);
|
||||
context.insert("description", &base.config.description);
|
||||
|
||||
// Fill out product information
|
||||
context.insert("full_name", &obj.product.full_name);
|
||||
context.insert("short_name", &obj.product.short_name);
|
||||
context.insert("price_usd", &obj.product.price_usd);
|
||||
context.insert("stock", &obj.product.stock);
|
||||
|
||||
// Insert navigation elements
|
||||
for i in &base.nav_elements {
|
||||
context.insert(&i.text.to_ascii_lowercase(), &i);
|
||||
}
|
||||
context.insert("product_classes", &base.product_classes);
|
||||
|
||||
// Write it to the file
|
||||
output_file
|
||||
.write_all(
|
||||
base.tera
|
||||
.render("products/product.html", &context)
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("write failed");
|
||||
}
|
||||
|
||||
fn render_page(data: RenderObject, nav_elements: &[NavLink], product_classes: &[NavLink]) {
|
||||
|
@ -230,15 +304,17 @@ fn render_page(data: RenderObject, nav_elements: &[NavLink], product_classes: &[
|
|||
Some(p) => {
|
||||
let mut estrogens = Vec::new();
|
||||
let mut anti_androgens = Vec::new();
|
||||
let mut progestogens = Vec::new();
|
||||
for i in p {
|
||||
match i.class {
|
||||
ProductClass::Estrogens => estrogens.push(i),
|
||||
ProductClass::AntiAndrogens => anti_androgens.push(i),
|
||||
ProductClass::Progestogens => todo!(),
|
||||
ProductClass::Progestogens => progestogens.push(i),
|
||||
}
|
||||
}
|
||||
context.insert("estrogens", &estrogens);
|
||||
context.insert("anti_androgens", &anti_androgens);
|
||||
context.insert("progestogens", &progestogens);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,4 +73,18 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if progestogens %}
|
||||
<h3 id="progestogens">Progestogens</h3>
|
||||
<div class="images">
|
||||
{% for product in progestogens %}
|
||||
<div class="gallery">
|
||||
<a target="_blank" href="/products/{{product.short_name}}">
|
||||
<img src="/assets/products/{{product.short_name}}.webp" width="600" height="400">
|
||||
</a>
|
||||
<div class="desc">{{ product.full_name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{# head content #}
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<!-- css style -->
|
||||
<link rel="preload" href="/style.css" as="style">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
|
||||
<!-- metadata -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="description" content="{{ description }}">
|
||||
|
||||
<!-- link preview card -->
|
||||
<meta name="og:title" content="{{ title }}">
|
||||
<meta name="twitter:title" content="{{ title }}">
|
||||
|
||||
<meta name="og:description" content="{{ description }}">
|
||||
<meta name="twitter:description" content="{{ description }}">
|
||||
|
||||
<!-- display settings & favicon -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="/assets/pill.svg">
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
{# Navigation #}
|
||||
<nav class="menu">
|
||||
<ul>
|
||||
<li><a {% if home.active %}class="active"{% endif %} href="{{ home.href }}">{{ home.text }}</a></li>
|
||||
<li><a {% if about.active %}class="active"{% endif %} href="{{ about.href }}">{{ about.text }}</a></li>
|
||||
<li class="dropdown">
|
||||
<a {% if products.active %}class="active"{% endif %} href="{{ products.href }}" class="dropbtn">{{ products.text }}</a>
|
||||
<div class="dropdown-content">
|
||||
{% for product in product_classes %}
|
||||
<a href="{{product.href}}">{{product.text}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
<li><a {% if contact.active %}class="active"{% endif %} href="{{ contact.href }}">{{ contact.text }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h2 class="product">{{ full_name }}</h2>
|
||||
|
||||
<img class="product" alt="{{full_name}}" src="/assets/products/{{short_name}}.webp">
|
||||
|
||||
<div class="product">
|
||||
<code class="price" id="price">${{price_usd}} USD</code>
|
||||
<br>
|
||||
<code class="price" id="stock">{{stock}} in stock</code>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue