refactor: clean up db stuff; start comments stuff
							parent
							
								
									f980115d45
								
							
						
					
					
						commit
						fed9c01370
					
				
							
								
								
									
										108
									
								
								src/db.rs
								
								
								
								
							
							
						
						
									
										108
									
								
								src/db.rs
								
								
								
								
							| 
						 | 
					@ -1,108 +0,0 @@
 | 
				
			||||||
use std::{error::Error, fmt};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use r2d2_sqlite::{
 | 
					 | 
				
			||||||
    rusqlite::{self, Row},
 | 
					 | 
				
			||||||
    SqliteConnectionManager,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub enum DbError {
 | 
					 | 
				
			||||||
    Pool(r2d2::Error),
 | 
					 | 
				
			||||||
    Db(rusqlite::Error),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl fmt::Display for DbError {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::Pool(_) => write!(f, "failed to acquire connection from pool"),
 | 
					 | 
				
			||||||
            Self::Db(_) => write!(f, "error while accessing the database"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Error for DbError {
 | 
					 | 
				
			||||||
    fn source(&self) -> Option<&(dyn Error + 'static)> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::Pool(err) => Some(err),
 | 
					 | 
				
			||||||
            Self::Db(err) => Some(err),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl From<r2d2::Error> for DbError {
 | 
					 | 
				
			||||||
    fn from(value: r2d2::Error) -> Self {
 | 
					 | 
				
			||||||
        Self::Pool(value)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl From<rusqlite::Error> for DbError {
 | 
					 | 
				
			||||||
    fn from(value: rusqlite::Error) -> Self {
 | 
					 | 
				
			||||||
        Self::Db(value)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize)]
 | 
					 | 
				
			||||||
pub struct Plant {
 | 
					 | 
				
			||||||
    id: i32,
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    species: String,
 | 
					 | 
				
			||||||
    description: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Plant {
 | 
					 | 
				
			||||||
    pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
 | 
					 | 
				
			||||||
        Ok(Self {
 | 
					 | 
				
			||||||
            id: row.get(0)?,
 | 
					 | 
				
			||||||
            name: row.get(1)?,
 | 
					 | 
				
			||||||
            species: row.get(2)?,
 | 
					 | 
				
			||||||
            description: row.get(3)?,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct NewPlant {
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    species: String,
 | 
					 | 
				
			||||||
    description: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn list_plants(pool: &DbPool) -> Result<Vec<Plant>, DbError> {
 | 
					 | 
				
			||||||
    let conn = pool.get()?;
 | 
					 | 
				
			||||||
    let mut stmt = conn.prepare("select * from plants")?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut plants = Vec::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for plant in stmt.query_map((), |row| Plant::from_row(row))? {
 | 
					 | 
				
			||||||
        plants.push(plant?);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(plants)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn insert_plant(pool: &DbPool, plant: &NewPlant) -> Result<Plant, DbError> {
 | 
					 | 
				
			||||||
    let conn = pool.get()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut stmt = conn.prepare(
 | 
					 | 
				
			||||||
        "insert into plants (name, species, description) values ($1, $2, $3) returning *",
 | 
					 | 
				
			||||||
    )?;
 | 
					 | 
				
			||||||
    Ok(
 | 
					 | 
				
			||||||
        stmt.query_row((&plant.name, &plant.species, &plant.description), |row| {
 | 
					 | 
				
			||||||
            Plant::from_row(row)
 | 
					 | 
				
			||||||
        })?,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn get_plant(pool: &DbPool, id: u32) -> Result<Option<Plant>, DbError> {
 | 
					 | 
				
			||||||
    let conn = pool.get()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut stmt = conn.prepare("select * from plants where id = $1")?;
 | 
					 | 
				
			||||||
    match stmt.query_row((id,), |row| Plant::from_row(row)) {
 | 
					 | 
				
			||||||
        Ok(plant) => Ok(Some(plant)),
 | 
					 | 
				
			||||||
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
 | 
					 | 
				
			||||||
        Err(err) => Err(DbError::Db(err)),
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					use r2d2_sqlite::rusqlite::{self, Row};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct Comment {
 | 
				
			||||||
 | 
					    id: i32,
 | 
				
			||||||
 | 
					    plant_id: i32,
 | 
				
			||||||
 | 
					    comment: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Comment {
 | 
				
			||||||
 | 
					    pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            id: row.get(0)?,
 | 
				
			||||||
 | 
					            plant_id: row.get(1)?,
 | 
				
			||||||
 | 
					            comment: row.get(2)?,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					mod comment;
 | 
				
			||||||
 | 
					mod plant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{error::Error, fmt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use comment::Comment;
 | 
				
			||||||
 | 
					pub use plant::{NewPlant, Plant};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum DbError {
 | 
				
			||||||
 | 
					    Pool(r2d2::Error),
 | 
				
			||||||
 | 
					    Db(rusqlite::Error),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for DbError {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Pool(_) => write!(f, "failed to acquire connection from pool"),
 | 
				
			||||||
 | 
					            Self::Db(_) => write!(f, "error while accessing the database"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Error for DbError {
 | 
				
			||||||
 | 
					    fn source(&self) -> Option<&(dyn Error + 'static)> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Pool(err) => Some(err),
 | 
				
			||||||
 | 
					            Self::Db(err) => Some(err),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<r2d2::Error> for DbError {
 | 
				
			||||||
 | 
					    fn from(value: r2d2::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::Pool(value)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<rusqlite::Error> for DbError {
 | 
				
			||||||
 | 
					    fn from(value: rusqlite::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::Db(value)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					use r2d2_sqlite::rusqlite::{self, Row};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{Comment, DbError, DbPool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct Plant {
 | 
				
			||||||
 | 
					    id: i32,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    species: String,
 | 
				
			||||||
 | 
					    description: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Plant {
 | 
				
			||||||
 | 
					    pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            id: row.get(0)?,
 | 
				
			||||||
 | 
					            name: row.get(1)?,
 | 
				
			||||||
 | 
					            species: row.get(2)?,
 | 
				
			||||||
 | 
					            description: row.get(3)?,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn all(pool: &DbPool) -> Result<Vec<Self>, DbError> {
 | 
				
			||||||
 | 
					        let conn = pool.get()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut stmt = conn.prepare("select * from plants")?;
 | 
				
			||||||
 | 
					        let plants: Result<Vec<_>, _> = stmt.query_map((), Self::from_row)?.collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(plants?)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn with_id(pool: &DbPool, id: i32) -> Result<Option<Self>, DbError> {
 | 
				
			||||||
 | 
					        let conn = pool.get()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut stmt = conn.prepare("select * from plants where id = $1")?;
 | 
				
			||||||
 | 
					        match stmt.query_row((id,), |row| Plant::from_row(row)) {
 | 
				
			||||||
 | 
					            Ok(plant) => Ok(Some(plant)),
 | 
				
			||||||
 | 
					            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
 | 
				
			||||||
 | 
					            Err(err) => Err(DbError::Db(err)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn comments(&self, pool: &DbPool) -> Result<Vec<Comment>, DbError> {
 | 
				
			||||||
 | 
					        let conn = pool.get()?;
 | 
				
			||||||
 | 
					        let mut stmt = conn.prepare("select * from plant_comments where plant_id = $1")?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let comments: Result<Vec<_>, _> = stmt.query_map((self.id,), Comment::from_row)?.collect();
 | 
				
			||||||
 | 
					        Ok(comments?)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct NewPlant {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    species: String,
 | 
				
			||||||
 | 
					    description: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl NewPlant {
 | 
				
			||||||
 | 
					    pub fn insert(self, pool: &DbPool) -> Result<Plant, DbError> {
 | 
				
			||||||
 | 
					        let conn = pool.get()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut stmt = conn.prepare(
 | 
				
			||||||
 | 
					            "insert into plants (name, species, description) values ($1, $2, $3) returning *",
 | 
				
			||||||
 | 
					        )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(stmt.query_row(
 | 
				
			||||||
 | 
					            (&self.name, &self.species, &self.description),
 | 
				
			||||||
 | 
					            Plant::from_row,
 | 
				
			||||||
 | 
					        )?)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -7,9 +7,10 @@ use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
 | 
				
			||||||
use tera::Tera;
 | 
					use tera::Tera;
 | 
				
			||||||
use tower_http::compression::CompressionLayer;
 | 
					use tower_http::compression::CompressionLayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MIGRATIONS: [&str; 2] = [
 | 
					const MIGRATIONS: [&str; 3] = [
 | 
				
			||||||
    include_str!("migrations/000_initial.sql"),
 | 
					    include_str!("migrations/000_initial.sql"),
 | 
				
			||||||
    include_str!("migrations/001_plants.sql"),
 | 
					    include_str!("migrations/001_plants.sql"),
 | 
				
			||||||
 | 
					    include_str!("migrations/002_comments.sql"),
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
const STATIC_FILES: [(&str, &'static str); 1] = [(
 | 
					const STATIC_FILES: [(&str, &'static str); 1] = [(
 | 
				
			||||||
    "htmx_2.0.4.min.js",
 | 
					    "htmx_2.0.4.min.js",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					create table comments (
 | 
				
			||||||
 | 
					    id integer primary key,
 | 
				
			||||||
 | 
					    plant_id integer references plants (id),
 | 
				
			||||||
 | 
					    comment text not null
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ use axum::{
 | 
				
			||||||
use tera::Context;
 | 
					use tera::Context;
 | 
				
			||||||
use tower_http::set_header::SetResponseHeaderLayer;
 | 
					use tower_http::set_header::SetResponseHeaderLayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::db;
 | 
					use crate::db::Plant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const HX_REQUEST_HEADER: &str = "HX-Request";
 | 
					const HX_REQUEST_HEADER: &str = "HX-Request";
 | 
				
			||||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
 | 
					const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ pub fn app(ctx: crate::Context) -> axum::Router {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
 | 
					async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
 | 
				
			||||||
    let plants = tokio::task::spawn_blocking(move || db::list_plants(&ctx.pool))
 | 
					    let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool))
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
        .unwrap();
 | 
					        .unwrap();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ use axum::{
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use tera::Context;
 | 
					use tera::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::db;
 | 
					use crate::db::{self, DbError, Plant};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::render_partial;
 | 
					use super::render_partial;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,15 +21,21 @@ pub fn app(ctx: crate::Context) -> axum::Router {
 | 
				
			||||||
async fn get_plant_page(
 | 
					async fn get_plant_page(
 | 
				
			||||||
    State(ctx): State<crate::Context>,
 | 
					    State(ctx): State<crate::Context>,
 | 
				
			||||||
    headers: HeaderMap,
 | 
					    headers: HeaderMap,
 | 
				
			||||||
    Path(plant_id): Path<u32>,
 | 
					    Path(plant_id): Path<i32>,
 | 
				
			||||||
) -> Html<String> {
 | 
					) -> Html<String> {
 | 
				
			||||||
    let plant = tokio::task::spawn_blocking(move || db::get_plant(&ctx.pool, plant_id))
 | 
					    let (plant, comments) = tokio::task::spawn_blocking(move || {
 | 
				
			||||||
        .await
 | 
					        let plant = Plant::with_id(&ctx.pool, plant_id)?.unwrap();
 | 
				
			||||||
        .unwrap()
 | 
					        let comments = plant.comments(&ctx.pool)?;
 | 
				
			||||||
        .unwrap();
 | 
					
 | 
				
			||||||
 | 
					        Ok::<_, DbError>((plant, comments))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut context = Context::new();
 | 
					    let mut context = Context::new();
 | 
				
			||||||
    context.insert("plant", &plant);
 | 
					    context.insert("plant", &plant);
 | 
				
			||||||
 | 
					    context.insert("comments", &comments);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let tmpl = if render_partial(&headers) {
 | 
					    let tmpl = if render_partial(&headers) {
 | 
				
			||||||
        "partials/plant_info.html"
 | 
					        "partials/plant_info.html"
 | 
				
			||||||
| 
						 | 
					@ -44,7 +50,7 @@ async fn post_plant(
 | 
				
			||||||
    State(ctx): State<crate::Context>,
 | 
					    State(ctx): State<crate::Context>,
 | 
				
			||||||
    Form(plant): Form<db::NewPlant>,
 | 
					    Form(plant): Form<db::NewPlant>,
 | 
				
			||||||
) -> Html<String> {
 | 
					) -> Html<String> {
 | 
				
			||||||
    let plant = tokio::task::spawn_blocking(move || db::insert_plant(&ctx.pool, &plant))
 | 
					    let plant = tokio::task::spawn_blocking(move || plant.insert(&ctx.pool))
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
        .unwrap();
 | 
					        .unwrap();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,3 +4,16 @@
 | 
				
			||||||
<p>{{ plant.species }}</p>
 | 
					<p>{{ plant.species }}</p>
 | 
				
			||||||
<h3>Description</h3>
 | 
					<h3>Description</h3>
 | 
				
			||||||
<p>{{ plant.description }}</p>
 | 
					<p>{{ plant.description }}</p>
 | 
				
			||||||
 | 
					<h3>Comments</h3>
 | 
				
			||||||
 | 
					<ul id="comments">
 | 
				
			||||||
 | 
					{% for comment in comments %}
 | 
				
			||||||
 | 
					    <li>
 | 
				
			||||||
 | 
					        {{ comment.comment }}
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					{% endfor %}
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
 | 
					<form hx-post="/plants/{{ plant.id }}/comments" hx-target="#comments" hx-swap="beforeend">
 | 
				
			||||||
 | 
					    <label for="comment">Comment:</label>
 | 
				
			||||||
 | 
					    <textarea id="comment" name="comment" rows=4></textarea></br>
 | 
				
			||||||
 | 
					    <input type="submit">
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue