catgirl-cooking/src/structures.rs

327 lines
11 KiB
Rust

use build_html::{self, Container, ContainerType, Html, HtmlContainer, HtmlPage};
use chrono::prelude::*;
use rocket::form::Form;
use serde_derive::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
use crate::io::write_html;
// Content strings to add to html
pub const TITLE: &str = r#"🥘 Catgirl Cooking"#;
pub const HEADER: &str = r#"🥘 Catgirl Cooking"#;
pub const DESCRIPTION: &str = r#"The cutest cooking site on the net :3"#;
pub const DETAILS: &str = r#"Absolutely no ads, tracking, or nazis, ever."#;
#[derive(Debug, Serialize, Deserialize)]
pub enum TimeUnits {
Hours,
Minutes,
Seconds,
}
impl fmt::Display for TimeUnits {
// This trait requires `fmt` with this exact signature.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write strictly the first element into the supplied output
// stream: `f`. Returns `fmt::Result` which indicates whether the
// operation succeeded or failed. Note that `write!` uses syntax which
// is very similar to `println!`.
match &self {
TimeUnits::Hours => write!(f, "hr."),
TimeUnits::Minutes => write!(f, "min."),
TimeUnits::Seconds => write!(f, "sec."),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum MeasurementUnits {
Grams,
Kilograms,
Pounds,
Litres,
Millilitres,
Gallons,
Ounces,
Pinches,
Drops,
Cups,
Tablespoons,
Teaspoons,
}
type mu = MeasurementUnits;
impl fmt::Display for MeasurementUnits {
// This trait requires `fmt` with this exact signature.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write strictly the first element into the supplied output
// stream: `f`. Returns `fmt::Result` which indicates whether the
// operation succeeded or failed. Note that `write!` uses syntax which
// is very similar to `println!`.
match &self {
mu::Grams => write!(f, "g"),
mu::Kilograms => write!(f, "kg"),
mu::Pounds => write!(f, "lbs"),
mu::Litres => write!(f, "L"),
mu::Millilitres => write!(f, "mL"),
mu::Gallons => write!(f, "gal"),
mu::Ounces => write!(f, "oz"),
mu::Pinches => write!(f, "pinch(es)"),
mu::Drops => write!(f, "drop(s)"),
mu::Cups => write!(f, "cup(s)"),
mu::Tablespoons => write!(f, "tablespoon(s)"),
mu::Teaspoons => write!(f, "teaspoon(s)"),
}
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Tag(pub char, pub String);
impl Tag {
pub fn new(s: String) -> Tag {
let mut fuck = s;
fuck.make_ascii_lowercase();
Tag('#', fuck)
}
pub fn to_string(&self) -> String {
let mut tag = String::new();
tag.push_str(&self.0.to_string());
tag.push_str(&self.1);
return tag;
}
pub fn from_string(s: String) -> Tag {
let hash = s.chars().nth(0).unwrap();
let mut text = s;
text.remove(0);
text.make_ascii_lowercase();
return Tag(hash, text);
}
pub fn display(&self) {
println!("{}{}", self.0, self.1);
}
}
#[derive(FromForm)]
pub struct RecipeForm {
pub recipe_name: String,
pub author_name: String,
pub tags: String,
pub prep_time: String,
pub cooking_time: String,
pub servings: String,
pub ingredients: String,
pub directions: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Recipe {
pub id: Uuid, // Unique recipe ID
pub name: String, // Full recipe name
pub shortcode: String,
pub prep_time: (i32, TimeUnits), // Preparation time: (value, units)
pub cooking_time: (i32, TimeUnits), // Cooking time: (value, units)
pub servings: u8, // How many servinge the recipe makes
pub ingredients: Vec<(i32, MeasurementUnits, String)>, // Vector of ingredients: (value, units, name)
pub directions: Vec<(u16, String)>, // List of instructions: (step #, details)
pub attribution: String, // Author name
pub posted_date: DateTime<Utc>, // Date the recipe was first posted
pub edited_date: Option<DateTime<Utc>>, // Date the recipe was last edited
pub tags: Option<Vec<Tag>>, // List of tags
}
impl Recipe {
pub fn example(name: &str, time: i32, directions: Vec<&str>, ingredients: Vec<&str>) -> Recipe {
let mut ingredients_list: Vec<(i32, MeasurementUnits, String)> = Vec::new();
for i in 0..ingredients.len() {
ingredients_list.push((
i.try_into().unwrap(),
MeasurementUnits::Litres,
ingredients[i].to_string(),
));
}
let mut directions_list: Vec<(u16, String)> = Vec::new();
for n in 0..directions.len() {
directions_list.push((n.try_into().unwrap(), directions[n].to_string()));
}
Recipe {
id: Uuid::new_v4(),
name: name.to_string(),
shortcode: construct_shortcode(name.to_string()),
prep_time: (time, TimeUnits::Minutes),
cooking_time: (time, TimeUnits::Hours),
servings: 4,
ingredients: ingredients_list,
directions: directions_list,
attribution: "Erin".to_string(),
posted_date: Utc::now(),
edited_date: None,
tags: None,
}
}
pub fn new(
name: String,
prep_time: (i32, TimeUnits),
cooking_time: (i32, TimeUnits),
servings: u8,
ingredients: Vec<(i32, MeasurementUnits, String)>,
directions: Vec<String>,
attribution: String,
tags: Option<Vec<Tag>>,
) -> Recipe {
let mut directions_list: Vec<(u16, String)> = Vec::new();
for i in 0..directions.len() {
directions_list.push((i.try_into().unwrap(), directions[i].to_string()));
}
Recipe {
id: Uuid::new_v4(),
name: name.clone(),
shortcode: construct_shortcode(name.clone()),
prep_time,
cooking_time,
servings,
ingredients,
directions: directions_list,
attribution,
posted_date: Utc::now(),
edited_date: None,
tags,
}
}
pub fn construct_page(&self) {
// Create the footer
let footer = Container::new(ContainerType::Footer)
.with_raw(r#"<hr>"#) // Line seperator
.with_link("/", "home") // Link to the root page
.with_link("/submit.html", "submit recipe") // Link to submission form
.with_link("/rss.xml", "rss") // Link the the Atom feed
// License info
.with_paragraph("Software licensed under the CNPLv7+")
.with_paragraph("Recipes under Public Domain");
// Metadata
let meta = Container::new(ContainerType::UnorderedList)
.with_attributes(vec![("class", "recipe")])
.with_paragraph(format!(
"<b>⏲️ Preparation time: </b> {} {}",
&self.prep_time.0, &self.prep_time.1
))
.with_paragraph(format!(
"<b>🍳 Cooking time:</b> {} {}",
&self.cooking_time.0, &self.cooking_time.1
))
.with_paragraph(format!("<b>🍽️ Servings:</b> {}", &self.servings));
// Ingredients
let mut ingredients_container =
Container::new(ContainerType::UnorderedList).with_attributes(vec![("class", "recipe")]);
for i in &self.ingredients {
let ingredient = format!("{} {} {}", &i.0, &i.1, &i.2);
ingredients_container.add_paragraph(ingredient);
}
// Directions
let mut directions_container =
Container::new(ContainerType::OrderedList).with_attributes(vec![("class", "recipe")]);
for n in &self.directions {
directions_container.add_paragraph(&n.1);
}
// Tags
let mut tags_html = String::new();
tags_html.push_str(r#"<p><b>Tags: </b><i>"#);
match &self.tags {
Some(list) => {
for x in 0..list.len() {
if x != list.len() - 1 {
tags_html.push_str(
format!("<a href='/tags/{}'>#{}</a>,", list[x].1, list[x].1).as_str(),
);
} else {
tags_html.push_str(
format!("<a href='/tags/{}'>#{}</a>", list[x].1, list[x].1).as_str(),
);
}
}
}
None => {
tags_html.push_str(r#"no tags!! :o"#);
}
}
tags_html.push_str(r#"</i></p>"#);
// Date
let edit_date = match &self.edited_date {
Some(date) => {
format!(
"<p><b>Last edited on: </b><i>{}-{}-{}</i></p>",
date.year(),
date.month(),
date.day()
)
}
None => "<p><i>No edits</o></p>".to_string(),
};
// Construct Main Page
let recipe_page = build_html::HtmlPage::new()
.with_head_link("/favicon.ico", "icon") // Favicon
.with_stylesheet("/style.css") // Link stylesheet
.with_meta(vec![("charset", "UTF-8")])
.with_meta(vec![
("name", "viewport"),
("content", "width=device-width, initial-scale=1"),
]) // Display stuff
.with_meta(vec![("name", "description"), ("content", DESCRIPTION)]) // Add the description
.with_title(TITLE)
.with_header(1, &self.name)
.with_raw(r#"<hr>"#)
.with_container(meta)
.with_header(2, r#"Ingredients:"#)
.with_container(ingredients_container)
.with_header(2, r#"Directions:"#)
.with_container(directions_container)
.with_header(2, r#"Misc."#)
.with_raw(format!("<p><b>Author: </b>{}</p>", &self.attribution).as_str())
.with_raw(tags_html)
.with_raw(format!(
"<p><b>Recipe added on: </b><i>{}-{}-{}</i></p>",
&self.posted_date.year(),
&self.posted_date.month(),
&self.posted_date.day()
))
.with_raw(edit_date)
.with_container(footer);
write_html(recipe_page.to_html_string(), "en", Some(&self.shortcode));
}
}
pub fn construct_shortcode(full_name: String) -> String {
let mut prev_name = full_name;
prev_name.make_ascii_lowercase();
return prev_name
.chars()
.map(|x| match x {
' ' => '-',
_ => x,
})
.collect();
}
// Struct for the users answer to a captcha
#[derive(Clone, Debug, FromForm)]
pub struct CaptchaAnswer {
pub text: String,
}