diff --git a/src/components/mod.rs b/src/components/mod.rs index d6447b2..0e97efe 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,5 @@ +pub mod trips; + pub mod crud { use async_trait::async_trait; diff --git a/src/components/trips/mod.rs b/src/components/trips/mod.rs new file mode 100644 index 0000000..015a6a2 --- /dev/null +++ b/src/components/trips/mod.rs @@ -0,0 +1 @@ +pub mod todos; diff --git a/src/components/trips/todos/list.rs b/src/components/trips/todos/list.rs new file mode 100644 index 0000000..d4397e3 --- /dev/null +++ b/src/components/trips/todos/list.rs @@ -0,0 +1,109 @@ +use maud::{html, Markup}; +use uuid::Uuid; + +use super::Todo; +use crate::components::view; +use crate::models::trips::Trip; + +#[derive(Debug)] +pub struct List<'a> { + pub trip: &'a Trip, + pub todos: &'a Vec, +} + +#[derive(Debug)] +pub struct BuildInput { + pub edit_todo: Option, +} + +impl<'a> view::View for List<'a> { + type Input = BuildInput; + + #[tracing::instrument] + fn build(&self, input: Self::Input) -> Markup { + html!( + div #todolist { + h1 ."text-xl" ."mb-5" { "Todos" } + ul + ."flex" + ."flex-col" + { + @for todo in self.todos { + @let state = input.edit_todo + .map(|id| if todo.id == id { + super::UiState::Edit + } else { + super::UiState::Default + }).unwrap_or(super::UiState::Default); + (todo.build(super::BuildInput{trip_id:self.trip.id, state})) + } + (NewTodo::build(&self.trip.id)) + } + } + ) + } +} + +pub struct NewTodo; + +impl NewTodo { + #[tracing::instrument] + pub fn build(trip_id: &Uuid) -> Markup { + html!( + li + ."flex" + ."flex-row" + ."justify-start" + ."items-stretch" + ."h-full" + { + form + name="new-todo" + id="new-todo" + action={ + "/trips/" (trip_id) + "/todo/new" + } + target="_self" + method="post" + hx-post={ + "/trips/" (trip_id) + "/todo/new" + } + hx-target="#todolist" + hx-swap="outerHTML" + {} + button + type="submit" + form="new-todo" + ."bg-green-200" + ."hover:bg-green-300" + ."flex" + ."flex-row" + ."aspect-square" + { + span + ."mdi" + ."m-auto" + ."mdi-plus" + ."text-xl" + {} + } + div + ."border-4" + ."p-1" + .grow + { + input + ."appearance-none" + ."w-full" + type="text" + form="new-todo" + id="new-todo-description" + name="new-todo-description" + {} + } + } + ) + } +} diff --git a/src/models/trips/todos.rs b/src/components/trips/todos/mod.rs similarity index 83% rename from src/models/trips/todos.rs rename to src/components/trips/todos/mod.rs index 44bfa14..19044f8 100644 --- a/src/models/trips/todos.rs +++ b/src/components/trips/todos/mod.rs @@ -1,3 +1,6 @@ +pub mod list; +pub use list::List; + use axum::{ body::BoxBody, extract::{Form, Path}, @@ -12,7 +15,7 @@ use uuid::Uuid; use crate::{ components::{ crud, route, - view::{self, *}, + view::{self, View}, }, htmx, models::Error, @@ -21,7 +24,7 @@ use crate::{ use async_trait::async_trait; -use super::Trip; +use crate::models::trips::Trip; #[derive(Debug, PartialEq, Eq)] pub enum State { @@ -74,7 +77,7 @@ impl TryFrom for Todo { } #[derive(Debug, Clone)] -pub struct TodoFilter { +pub struct Filter { pub trip_id: Uuid, } @@ -86,13 +89,13 @@ impl Todo { #[async_trait] impl crud::Read for Todo { - type Filter = TodoFilter; + type Filter = Filter; type Id = Uuid; async fn findall( ctx: &Context, pool: &sqlite::Pool, - filter: TodoFilter, + filter: Filter, ) -> Result, Error> { let trip_id_param = filter.trip_id.to_string(); let user_id = ctx.user.id.to_string(); @@ -129,7 +132,7 @@ impl crud::Read for Todo { async fn find( ctx: &Context, pool: &sqlite::Pool, - filter: TodoFilter, + filter: Filter, todo_id: Uuid, ) -> Result, Error> { let trip_id_param = filter.trip_id.to_string(); @@ -171,7 +174,7 @@ pub struct TodoNew { #[async_trait] impl crud::Create for Todo { type Id = Uuid; - type Filter = TodoFilter; + type Filter = Filter; type Info = TodoNew; async fn create( @@ -212,7 +215,7 @@ impl crud::Create for Todo { } #[derive(Debug)] -pub enum TodoUpdate { +pub enum Update { State(State), Description(String), } @@ -220,8 +223,8 @@ pub enum TodoUpdate { #[async_trait] impl crud::Update for Todo { type Id = Uuid; - type Filter = TodoFilter; - type Update = TodoUpdate; + type Filter = Filter; + type Update = Update; #[tracing::instrument] async fn update( @@ -235,7 +238,7 @@ impl crud::Update for Todo { let trip_id_param = filter.trip_id.to_string(); let todo_id_param = id.to_string(); match update { - TodoUpdate::State(state) => { + Update::State(state) => { let done = state == State::Done; let result = crate::query_one!( @@ -267,7 +270,7 @@ impl crud::Update for Todo { Ok(result) } - TodoUpdate::Description(new_description) => { + Update::Description(new_description) => { let user_id = ctx.user.id.to_string(); let trip_id_param = filter.trip_id.to_string(); let todo_id_param = id.to_string(); @@ -309,15 +312,10 @@ impl crud::Update for Todo { #[async_trait] impl crud::Delete for Todo { type Id = Uuid; - type Filter = TodoFilter; + type Filter = Filter; #[tracing::instrument] - async fn delete<'c, T>( - ctx: &Context, - db: T, - filter: &TodoFilter, - id: Uuid, - ) -> Result + async fn delete<'c, T>(ctx: &Context, db: T, filter: &Filter, id: Uuid) -> Result where T: sqlx::Acquire<'c, Database = sqlx::Sqlite> + Send + std::fmt::Debug, { @@ -348,19 +346,19 @@ impl crud::Delete for Todo { } #[derive(Debug, PartialEq, Eq)] -pub enum TodoUiState { +pub enum UiState { Default, Edit, } #[derive(Debug)] -pub struct TodoBuildInput { +pub struct BuildInput { pub trip_id: Uuid, - pub state: TodoUiState, + pub state: UiState, } impl view::View for Todo { - type Input = TodoBuildInput; + type Input = BuildInput; #[tracing::instrument] fn build(&self, input: Self::Input) -> Markup { @@ -375,7 +373,7 @@ impl view::View for Todo { ."bg-red-50"[!done] ."h-full" { - @if input.state == TodoUiState::Edit { + @if input.state == UiState::Edit { form name="edit-todo" id="edit-todo" @@ -586,7 +584,7 @@ impl route::Create for Todo { let _todo_item = ::create( &ctx, &state.database_pool, - TodoFilter { trip_id }, + Filter { trip_id }, TodoNew { description: form.description, }, @@ -601,11 +599,11 @@ impl route::Create for Todo { })), Some(mut trip) => { trip.load_todos(&ctx, &state.database_pool).await?; - Ok(crate::models::trips::todos::TodoList { + Ok(list::List { trip: &trip, todos: &trip.todos(), } - .build(None) + .build(list::BuildInput { edit_todo: None }) .into_response()) } } @@ -633,7 +631,7 @@ impl route::Delete for Todo { let deleted = ::delete( &ctx, &state.database_pool, - &TodoFilter { trip_id }, + &Filter { trip_id }, todo_id, ) .await?; @@ -651,109 +649,13 @@ impl route::Delete for Todo { })), Some(mut trip) => { trip.load_todos(&ctx, &state.database_pool).await?; - Ok(TodoList { + Ok(list::List { trip: &trip, todos: &trip.todos(), } - .build(None) + .build(list::BuildInput { edit_todo: None }) .into_response()) } } } } - -pub struct NewTodo; - -impl NewTodo { - #[tracing::instrument] - pub fn build(trip_id: &Uuid) -> Markup { - html!( - li - ."flex" - ."flex-row" - ."justify-start" - ."items-stretch" - ."h-full" - { - form - name="new-todo" - id="new-todo" - action={ - "/trips/" (trip_id) - "/todo/new" - } - target="_self" - method="post" - hx-post={ - "/trips/" (trip_id) - "/todo/new" - } - hx-target="#todolist" - hx-swap="outerHTML" - {} - button - type="submit" - form="new-todo" - ."bg-green-200" - ."hover:bg-green-300" - ."flex" - ."flex-row" - ."aspect-square" - { - span - ."mdi" - ."m-auto" - ."mdi-plus" - ."text-xl" - {} - } - div - ."border-4" - ."p-1" - .grow - { - input - ."appearance-none" - ."w-full" - type="text" - form="new-todo" - id="new-todo-description" - name="new-todo-description" - {} - } - } - ) - } -} - -#[derive(Debug)] -pub struct TodoList<'a> { - pub trip: &'a Trip, - pub todos: &'a Vec, -} - -impl<'a> TodoList<'a> { - #[tracing::instrument] - pub fn build(&self, edit_todo: Option) -> Markup { - html!( - div #todolist { - h1 ."text-xl" ."mb-5" { "Todos" } - ul - ."flex" - ."flex-col" - { - @for todo in self.todos { - @let state = edit_todo - .map(|id| if todo.id == id { - TodoUiState::Edit - } else { - TodoUiState::Default - }).unwrap_or(TodoUiState::Default); - (todo.build(TodoBuildInput{trip_id:self.trip.id, state})) - } - (NewTodo::build(&self.trip.id)) - } - } - ) - } -} diff --git a/src/models/trips/mod.rs b/src/models/trips/mod.rs index c4a8ee4..c359ca1 100644 --- a/src/models/trips/mod.rs +++ b/src/models/trips/mod.rs @@ -14,8 +14,6 @@ use serde::{Deserialize, Serialize}; use time; use uuid::Uuid; -pub mod todos; - // #[macro_use] // mod macros { // macro_rules! build_state_query { @@ -512,7 +510,7 @@ pub struct Trip { pub temp_min: Option, pub temp_max: Option, pub comment: Option, - pub todos: Option>, + pub todos: Option>, pub types: Option>, pub categories: Option>, } @@ -999,7 +997,7 @@ impl Trip { } #[tracing::instrument] - pub fn todos(&self) -> &Vec { + pub fn todos(&self) -> &Vec { self.todos.as_ref().expect("you need to call load_todos()") } @@ -1021,8 +1019,14 @@ impl Trip { #[tracing::instrument] pub async fn load_todos(&mut self, ctx: &Context, pool: &sqlite::Pool) -> Result<(), Error> { - self.todos = - Some(todos::Todo::findall(ctx, pool, todos::TodoFilter { trip_id: self.id }).await?); + self.todos = Some( + crate::components::trips::todos::Todo::findall( + ctx, + pool, + crate::components::trips::todos::Filter { trip_id: self.id }, + ) + .await?, + ); Ok(()) } diff --git a/src/routing/mod.rs b/src/routing/mod.rs index c9b0517..812ff3c 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -152,16 +152,16 @@ pub fn router(state: AppState) -> Router { .route("/:id/todo/:id/edit/save", post(trip_todo_edit_save)) .route("/:id/todo/:id/edit/cancel", post(trip_todo_edit_cancel)) .route( - &::with_prefix( + &::with_prefix( "/:id/todo", ), - post(::create), + post(::create), ) .route( - &::with_prefix( + &::with_prefix( "/:id/todo", ), - post(::delete), + post(::delete), ), ) .nest( diff --git a/src/routing/routes.rs b/src/routing/routes.rs index 216bb43..d5cb33d 100644 --- a/src/routing/routes.rs +++ b/src/routing/routes.rs @@ -5,9 +5,10 @@ use axum::{ Form, }; +use crate::components; use crate::components::crud::*; +use crate::components::trips::todos; use crate::components::view::*; -use crate::models::trips::todos::{TodoBuildInput, TodoFilter, TodoUpdate}; use crate::view::Component; @@ -437,10 +438,10 @@ pub async fn trip( state.client_state.active_category_id = trip_query.category; if let Some(delete_todo) = trip_query.delete_todo { - let deleted = models::trips::todos::Todo::delete( + let deleted = components::trips::todos::Todo::delete( &ctx, &state.database_pool, - &TodoFilter { trip_id: id }, + &todos::Filter { trip_id: id }, delete_todo, ) .await?; @@ -1275,19 +1276,19 @@ pub async fn trip_todo_done_htmx( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - models::trips::todos::Todo::update( + components::trips::todos::Todo::update( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, - TodoUpdate::State(models::trips::todos::State::Done), + todos::Update::State(components::trips::todos::State::Done), ) .await?; - let todo_item = models::trips::todos::Todo::find( + let todo_item = components::trips::todos::Todo::find( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, ) .await? @@ -1297,9 +1298,9 @@ pub async fn trip_todo_done_htmx( }) })?; - Ok(todo_item.build(TodoBuildInput { + Ok(todo_item.build(todos::BuildInput { trip_id, - state: models::trips::todos::TodoUiState::Default, + state: components::trips::todos::UiState::Default, })) } @@ -1311,12 +1312,12 @@ pub async fn trip_todo_done( headers: HeaderMap, ) -> Result { let ctx = Context::build(current_user); - models::trips::todos::Todo::update( + components::trips::todos::Todo::update( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, - TodoUpdate::State(models::trips::todos::State::Done), + todos::Update::State(components::trips::todos::State::Done), ) .await?; @@ -1330,19 +1331,19 @@ pub async fn trip_todo_undone_htmx( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - models::trips::todos::Todo::update( + components::trips::todos::Todo::update( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, - TodoUpdate::State(models::trips::todos::State::Todo), + todos::Update::State(components::trips::todos::State::Todo), ) .await?; - let todo_item = models::trips::todos::Todo::find( + let todo_item = components::trips::todos::Todo::find( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, ) .await? @@ -1352,9 +1353,9 @@ pub async fn trip_todo_undone_htmx( }) })?; - Ok(todo_item.build(TodoBuildInput { + Ok(todo_item.build(todos::BuildInput { trip_id, - state: models::trips::todos::TodoUiState::Default, + state: components::trips::todos::UiState::Default, })) } @@ -1366,12 +1367,12 @@ pub async fn trip_todo_undone( headers: HeaderMap, ) -> Result { let ctx = Context::build(current_user); - models::trips::todos::Todo::update( + components::trips::todos::Todo::update( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, - TodoUpdate::State(models::trips::todos::State::Todo), + todos::Update::State(components::trips::todos::State::Todo), ) .await?; @@ -1393,10 +1394,10 @@ pub async fn trip_todo_edit( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - let todo_item = models::trips::todos::Todo::find( + let todo_item = components::trips::todos::Todo::find( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, ) .await?; @@ -1406,9 +1407,9 @@ pub async fn trip_todo_edit( message: format!("todo with id {todo_id} not found"), })), Some(todo_item) => Ok(todo_item - .build(TodoBuildInput { + .build(todos::BuildInput { trip_id, - state: models::trips::todos::TodoUiState::Edit, + state: components::trips::todos::UiState::Edit, }) .into_response()), } @@ -1423,12 +1424,12 @@ pub async fn trip_todo_edit_save( Form(form): Form, ) -> Result { let ctx = Context::build(current_user); - let todo_item = models::trips::todos::Todo::update( + let todo_item = components::trips::todos::Todo::update( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, - TodoUpdate::Description(form.description), + todos::Update::Description(form.description), ) .await?; @@ -1439,9 +1440,9 @@ pub async fn trip_todo_edit_save( Some(todo_item) => { if htmx::is_htmx(&headers) { Ok(todo_item - .build(TodoBuildInput { + .build(todos::BuildInput { trip_id, - state: models::trips::todos::TodoUiState::Default, + state: components::trips::todos::UiState::Default, }) .into_response()) } else { @@ -1459,10 +1460,10 @@ pub async fn trip_todo_edit_cancel( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - let todo_item = models::trips::todos::Todo::find( + let todo_item = components::trips::todos::Todo::find( &ctx, &state.database_pool, - TodoFilter { trip_id }, + todos::Filter { trip_id }, todo_id, ) .await?; @@ -1472,9 +1473,9 @@ pub async fn trip_todo_edit_cancel( message: format!("todo with id {todo_id} not found"), })), Some(todo_item) => Ok(todo_item - .build(TodoBuildInput { + .build(todos::BuildInput { trip_id, - state: models::trips::todos::TodoUiState::Default, + state: components::trips::todos::UiState::Default, }) .into_response()), } diff --git a/src/view/trip/mod.rs b/src/view/trip/mod.rs index 4e9a9a1..abe58d7 100644 --- a/src/view/trip/mod.rs +++ b/src/view/trip/mod.rs @@ -11,6 +11,8 @@ pub struct TripManager; pub mod packagelist; pub mod types; +use crate::components::view::View; + impl TripManager { #[tracing::instrument] pub fn build(trips: Vec) -> Markup { @@ -371,7 +373,7 @@ impl Trip { } } (TripInfo::build(trip_edit_attribute, trip)) - (crate::models::trips::todos::TodoList{todos: trip.todos(), trip: &trip}.build(edit_todo)) + (crate::components::trips::todos::List{todos: trip.todos(), trip: &trip}.build(crate::components::trips::todos::list::BuildInput { edit_todo})) (TripComment::build(trip)) (TripItems::build(active_category, trip)) }