296 lines
10 KiB
Rust
296 lines
10 KiB
Rust
use uuid::Uuid;
|
|
use chrono::prelude::*;
|
|
use serde_derive::{Deserialize, Serialize};
|
|
use build_html::{self, Html, HtmlContainer, ContainerType, Container, HtmlPage};
|
|
use std::fmt;
|
|
use rocket::form::Form;
|
|
|
|
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 {
|
|
Tag('#', s)
|
|
}
|
|
|
|
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 name: String,
|
|
pub author: 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("/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,
|
|
}
|