feat: added event form & POST route
							parent
							
								
									3add93bdb2
								
							
						
					
					
						commit
						adef5c1fd5
					
				|  | @ -1,6 +1,6 @@ | |||
| use std::{fmt::Display, str::FromStr}; | ||||
| 
 | ||||
| use chrono::Utc; | ||||
| use chrono::NaiveDate; | ||||
| use r2d2_sqlite::rusqlite::{ | ||||
|     self, | ||||
|     types::{FromSql, FromSqlError}, | ||||
|  | @ -8,6 +8,10 @@ use r2d2_sqlite::rusqlite::{ | |||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use super::{DbError, DbPool}; | ||||
| 
 | ||||
| pub const EVENT_TYPES: [&str; 1] = ["Watering"]; | ||||
| 
 | ||||
| #[derive(Serialize, Deserialize)] | ||||
| pub enum EventType { | ||||
|     Watering, | ||||
|  | @ -63,17 +67,17 @@ pub struct Event { | |||
|     id: i32, | ||||
|     plant_id: i32, | ||||
|     event_type: EventType, | ||||
|     time: chrono::DateTime<Utc>, | ||||
|     date: NaiveDate, | ||||
|     description: String, | ||||
| } | ||||
| 
 | ||||
| impl Event { | ||||
|     pub fn from_row(row: Row<'_>) -> rusqlite::Result<Self> { | ||||
|     pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> { | ||||
|         Ok(Self { | ||||
|             id: row.get("id")?, | ||||
|             plant_id: row.get("plant_id")?, | ||||
|             event_type: row.get("event_type")?, | ||||
|             time: row.get("time")?, | ||||
|             date: row.get("date")?, | ||||
|             description: row.get("description")?, | ||||
|         }) | ||||
|     } | ||||
|  | @ -83,6 +87,26 @@ impl Event { | |||
| pub struct NewEvent { | ||||
|     plant_id: i32, | ||||
|     event_type: EventType, | ||||
|     time: chrono::DateTime<Utc>, | ||||
|     date: NaiveDate, | ||||
|     description: String, | ||||
| } | ||||
| 
 | ||||
| impl NewEvent { | ||||
|     pub fn insert(self, pool: &DbPool) -> Result<Event, DbError> { | ||||
|         let conn = pool.get()?; | ||||
| 
 | ||||
|         let mut stmt = conn.prepare( | ||||
|             "insert into events (plant_id, event_type, date, description) values ($1, $2, $3, $4) returning *", | ||||
|         )?; | ||||
| 
 | ||||
|         Ok(stmt.query_row( | ||||
|             ( | ||||
|                 &self.plant_id, | ||||
|                 &self.event_type, | ||||
|                 &self.date, | ||||
|                 &self.description, | ||||
|             ), | ||||
|             Event::from_row, | ||||
|         )?) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ use r2d2_sqlite::{rusqlite, SqliteConnectionManager}; | |||
| use std::{error::Error, fmt}; | ||||
| 
 | ||||
| pub use comment::{Comment, NewComment}; | ||||
| pub use event::{Event, EventType, NewEvent, EVENT_TYPES}; | ||||
| pub use plant::{NewPlant, Plant}; | ||||
| 
 | ||||
| pub type DbPool = r2d2::Pool<SqliteConnectionManager>; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| use r2d2_sqlite::rusqlite::{self, Row}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use super::{Comment, DbError, DbPool}; | ||||
| use super::{Comment, DbError, DbPool, Event}; | ||||
| 
 | ||||
| #[derive(Serialize)] | ||||
| pub struct Plant { | ||||
|  | @ -55,6 +55,14 @@ impl Plant { | |||
|         let comments: Result<Vec<_>, _> = stmt.query_map((self.id,), Comment::from_row)?.collect(); | ||||
|         Ok(comments?) | ||||
|     } | ||||
| 
 | ||||
|     pub fn events(&self, pool: &DbPool) -> Result<Vec<Event>, DbError> { | ||||
|         let conn = pool.get()?; | ||||
|         let mut stmt = conn.prepare("select * from events where plant_id = $1")?; | ||||
| 
 | ||||
|         let events: Result<Vec<_>, _> = stmt.query_map((self.id,), Event::from_row)?.collect(); | ||||
|         Ok(events?) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl NewPlant { | ||||
|  |  | |||
|  | @ -72,6 +72,14 @@ fn load_templates() -> Tera { | |||
|             "partials/comment_li.html", | ||||
|             include_str!("templates/partials/comment_li.html"), | ||||
|         ), | ||||
|         ( | ||||
|             "partials/event_li.html", | ||||
|             include_str!("templates/partials/event_li.html"), | ||||
|         ), | ||||
|         ( | ||||
|             "macros/event.html", | ||||
|             include_str!("templates/macros/event.html"), | ||||
|         ), | ||||
|     ]) | ||||
|     .unwrap(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,6 @@ create table events ( | |||
|         references plants (id) | ||||
|         on delete cascade, | ||||
|     event_type text not null, | ||||
|     time text not null, | ||||
|     date text not null, | ||||
|     description text not null | ||||
| ); | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ use tera::Context; | |||
| 
 | ||||
| use crate::db::{Comment, NewComment}; | ||||
| 
 | ||||
| pub fn app(ctx: crate::Context) -> axum::Router { | ||||
|     Router::new().route("/", post(post_comment)).with_state(ctx) | ||||
| pub fn app() -> axum::Router<crate::Context> { | ||||
|     Router::new().route("/", post(post_comment)) | ||||
| } | ||||
| 
 | ||||
| async fn post_comment( | ||||
|  |  | |||
|  | @ -0,0 +1,21 @@ | |||
| use axum::{extract::State, response::Html, routing::post, Form, Router}; | ||||
| use tera::Context; | ||||
| 
 | ||||
| use crate::db; | ||||
| 
 | ||||
| pub fn app() -> axum::Router<crate::Context> { | ||||
|     Router::new().route("/", post(post_event)) | ||||
| } | ||||
| 
 | ||||
| async fn post_event( | ||||
|     State(ctx): State<crate::Context>, | ||||
|     Form(event): Form<db::NewEvent>, | ||||
| ) -> super::Result<Html<String>> { | ||||
|     let event = tokio::task::spawn_blocking(move || event.insert(&ctx.pool)) | ||||
|         .await | ||||
|         .unwrap()?; | ||||
| 
 | ||||
|     let mut context = Context::new(); | ||||
|     context.insert("event", &event); | ||||
|     Ok(Html(ctx.tera.render("partials/event_li.html", &context)?)) | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| mod comments; | ||||
| mod error; | ||||
| mod events; | ||||
| mod plants; | ||||
| 
 | ||||
| use axum::{ | ||||
|  | @ -32,9 +33,10 @@ pub fn render_partial(headers: &HeaderMap) -> bool { | |||
| pub fn app(ctx: crate::Context) -> axum::Router { | ||||
|     let mut router = Router::new() | ||||
|         .route("/", get(get_index)) | ||||
|         .with_state(ctx.clone()) | ||||
|         .nest("/plants", plants::app(ctx.clone())) | ||||
|         .nest("/comments", comments::app(ctx.clone())); | ||||
|         .nest("/plants", plants::app()) | ||||
|         .nest("/comments", comments::app()) | ||||
|         .nest("/events", events::app()) | ||||
|         .with_state(ctx.clone()); | ||||
| 
 | ||||
|     for (name, content) in crate::STATIC_FILES { | ||||
|         router = router.route(&format!("/static/{}", name), get(content)) | ||||
|  |  | |||
|  | @ -11,11 +11,10 @@ use crate::db::{self, DbError, Plant}; | |||
| 
 | ||||
| use super::{error::AppError, render_partial}; | ||||
| 
 | ||||
| pub fn app(ctx: crate::Context) -> axum::Router { | ||||
| pub fn app() -> axum::Router<crate::Context> { | ||||
|     Router::new() | ||||
|         .route("/:id", get(get_plant_page)) | ||||
|         .route("/", post(post_plant)) | ||||
|         .with_state(ctx) | ||||
| } | ||||
| 
 | ||||
| async fn get_plant_page( | ||||
|  | @ -28,8 +27,9 @@ async fn get_plant_page( | |||
| 
 | ||||
|         if let Some(plant) = plant { | ||||
|             let comments = plant.comments(&ctx.pool)?; | ||||
|             let events = plant.events(&ctx.pool)?; | ||||
| 
 | ||||
|             Ok::<_, DbError>(Some((plant, comments))) | ||||
|             Ok::<_, DbError>(Some((plant, comments, events))) | ||||
|         } else { | ||||
|             Ok(None) | ||||
|         } | ||||
|  | @ -38,10 +38,12 @@ async fn get_plant_page( | |||
|     .unwrap()?; | ||||
| 
 | ||||
|     match res { | ||||
|         Some((plant, comments)) => { | ||||
|         Some((plant, comments, events)) => { | ||||
|             let mut context = Context::new(); | ||||
|             context.insert("plant", &plant); | ||||
|             context.insert("comments", &comments); | ||||
|             context.insert("events", &events); | ||||
|             context.insert("event_types", &db::EVENT_TYPES); | ||||
| 
 | ||||
|             let tmpl = if render_partial(&headers) { | ||||
|                 "partials/plant_info.html" | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| {% macro li(event) %} | ||||
| <li> | ||||
|     <div class="event"> | ||||
|         <b>{{ event.event_type }}</b> | ||||
|         <p>{{ event.date }}</p> | ||||
|         <p>{{ event.description }}</p> | ||||
|     </div> | ||||
| </li> | ||||
| {% endmacro li %} | ||||
| 
 | ||||
| {% macro form(plant_id, target="#events_ul") %} | ||||
| <form hx-post="/events" hx-target="{{ target }}" hx-swap="beforeend"> | ||||
|     <input type="hidden" id="plant_id" name="plant_id" value="{{ plant_id }}"> | ||||
|     <label for="event_type">Type:</label> | ||||
|     <select id="event_type" name="event_type"> | ||||
|         {% for type in event_types %} | ||||
|         <option value="{{ type }}">{{ type }}</option> | ||||
|         {% endfor %} | ||||
|     </select> | ||||
|     <label for="date">Date:</label> | ||||
|     <input type="date" id="date" name="date"> | ||||
|     <label for="description">Description:</label> | ||||
|     <textarea id="description" name="description" rows=2></textarea></br> | ||||
|     <input type="submit"> | ||||
| </form> | ||||
| {% endmacro form %} | ||||
|  | @ -0,0 +1,2 @@ | |||
| {% import "macros/event.html" as macros_event %} | ||||
| {{ macros_event::li(event=event) }} | ||||
|  | @ -1,9 +1,20 @@ | |||
| {% import "macros/event.html" as macros_event %} | ||||
| 
 | ||||
| <h1>Calathea</h1> | ||||
| <h2>{{ plant.name }}</h2> | ||||
| <h3>Species</h3> | ||||
| <p>{{ plant.species }}</p> | ||||
| <h3>Description</h3> | ||||
| <p>{{ plant.description }}</p> | ||||
| <h3>Events</h3> | ||||
| <div id="events"> | ||||
|     <ul id="events_ul"> | ||||
|         {% for event in events %} | ||||
|             {{ macros_event::li(event=event) }} | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
|     {{ macros_event::form(plant_id=plant.id) }} | ||||
| </div> | ||||
| <h3>Comments</h3> | ||||
| <ul id="comments"> | ||||
| {% for comment in comments %} | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| {% import "macros/event.html" as macros_event %} | ||||
| 
 | ||||
| {% extends "base.html" %} | ||||
| {% block content %} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue