From b8da7f7d27a6596e9b7f94355e718a8e0372c461 Mon Sep 17 00:00:00 2001 From: Mara-161 Date: Sun, 1 Mar 2026 15:06:27 +0100 Subject: [PATCH 1/3] chore: Code quality improvements --- packages/api/src/routes/events.rs | 31 ++++++++-------- packages/api/src/routes/events/invitations.rs | 13 +++++-- packages/api/src/routes/groups.rs | 18 +++++----- packages/api/src/routes/todo_list.rs | 8 ++++- packages/api/src/routes/todo_list/invite.rs | 12 ++++++- packages/api/src/routes/todos.rs | 9 +++-- packages/api/src/routes/users.rs | 5 +-- packages/api/src/server/auth.rs | 10 ++++++ .../src/components/ui/events/eventlist.rs | 36 +++++++++++++------ .../src/views/groups/group_detailed.rs | 23 ++++++------ .../frontend/src/views/groups/group_view.rs | 7 ++-- 11 files changed, 115 insertions(+), 57 deletions(-) diff --git a/packages/api/src/routes/events.rs b/packages/api/src/routes/events.rs index 63bb4f51..eba328d1 100644 --- a/packages/api/src/routes/events.rs +++ b/packages/api/src/routes/events.rs @@ -1,11 +1,9 @@ +#[cfg(feature = "server")] use crate::server; #[cfg(feature = "server")] use dioxus::server::axum::Extension; use dioxus::{fullstack::NoContent, prelude::*}; use entity::event::PartialEventModel; -use entity::links::EventUserMembers; -use entity::prelude::*; - pub mod invitations; #[get("/api/events?mindate&maxdate", ext: Extension, auth: Extension)] @@ -15,6 +13,7 @@ pub async fn list_events( ) -> Result, ServerFnError> { use entity::event::Column as EventColumn; use entity::group::Column as GroupColumn; + use entity::group::Entity as Group; use entity::invitation::Column as InvitationColumn; use entity::shared_group_event::Column as GroupEventColumn; use sea_orm::{ @@ -113,7 +112,7 @@ pub async fn retrieve_event(event_id: i32) -> Result Result { .one(&ext.database) .await .or_internal_server_error("Error loading event from database")? - .or_not_found("event not found")?; + .or_not_found("Event not found")?; (event.owner_id == user_id).or_unauthorized("Unauthorized to delete this event")?; @@ -231,7 +230,7 @@ pub async fn update_event( .one(&ext.database) .await .or_internal_server_error("Failed to load event")? - .or_not_found("event somehow not found")?; + .or_not_found("Event not found")?; let owner = event.owner_id; @@ -265,6 +264,7 @@ pub async fn update_event( #[get("/api/events/{event_id}/groups", ext: Extension)] pub async fn list_event_groups(event_id: i32) -> Result, ServerFnError> { use entity::event::Entity as Event; + use entity::group::Entity as Group; use sea_orm::{EntityTrait, ModelTrait}; let event = Event::find_by_id(event_id) @@ -345,33 +345,32 @@ pub async fn remove_event_from_group( "Failed to remove event {event_id} from group {group_id}" ))?; - //delete_event(event_id).await?; - Ok(NoContent) } #[get("/api/events/{event_id}/members", ext: Extension)] pub async fn list_event_members(event_id: i32) -> Result, ServerFnError> { + use entity::links::EventUserMembers; use sea_orm::EntityTrait; use sea_orm::ModelTrait; let event = entity::event::Entity::find_by_id(event_id) .one(&ext.database) .await - .or_internal_server_error("could not load event")? - .or_not_found("could not find event")?; + .or_internal_server_error("Error loading event from database")? + .or_not_found("Event not found")?; let owner = entity::user::Entity::find_by_id(event.owner_id) .one(&ext.database) .await - .or_internal_server_error("could not load event")? - .or_not_found("could not find event")?; + .or_internal_server_error("Error loading owner from database")? + .or_not_found("Owner not found")?; let mut shares = event .find_linked(EventUserMembers) .all(&ext.database) .await - .or_internal_server_error("failed to retrieve other members")?; + .or_internal_server_error("Error retrieving members")?; shares.push(owner); @@ -389,13 +388,13 @@ pub async fn leave_event(event_id: i32) -> Result { .filter(entity::shared_friend_event::Column::UserId.eq(user.id)) .one(&ext.database) .await - .or_internal_server_error("Failed to retrieve info from server")? - .or_not_found("user is not in this event")?; + .or_internal_server_error("Error loading event from database")? + .or_not_found("User is not in this event")?; shared_event .delete(&ext.database) .await - .or_internal_server_error("could not leave event")?; + .or_internal_server_error("Error leaving event")?; Ok(NoContent) } diff --git a/packages/api/src/routes/events/invitations.rs b/packages/api/src/routes/events/invitations.rs index 9909fdc1..01e0ef4c 100644 --- a/packages/api/src/routes/events/invitations.rs +++ b/packages/api/src/routes/events/invitations.rs @@ -1,15 +1,16 @@ +#[cfg(feature = "server")] use crate::server; use dioxus::{fullstack::NoContent, prelude::*}; -use entity::{links::FriendEvents, prelude::*}; #[cfg(feature = "server")] use dioxus::server::axum::Extension; -use entity::invitation::InvitationStatus; -//TODO maybe optimize with joined queries, to not load event and user seperatley #[get("/api/events/invitations", ext: Extension, auth: Extension)] pub async fn list_received_invites() -> Result, ServerFnError> { + use entity::invitation::Entity as Invitation; + use entity::invitation::InvitationStatus; use sea_orm::{ColumnTrait, ModelTrait, QueryFilter}; + let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; let invites = user @@ -28,6 +29,7 @@ pub async fn send_invite( event_id: i32, ) -> Result { use crate::server::events::can_invite_user_to_event; + use entity::invitation::InvitationStatus; use sea_orm::{ActiveModelTrait, EntityTrait, Set, TryIntoModel}; use server::auth::find_user_by_email; @@ -72,6 +74,8 @@ pub async fn send_invite( #[post("/api/events/invitations/{invitation_id}/accept", ext: Extension, auth: Extension)] pub async fn accept_invite(invitation_id: i32) -> Result { + use entity::invitation::Entity as Invitation; + use entity::invitation::InvitationStatus; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -109,6 +113,8 @@ pub async fn accept_invite(invitation_id: i32) -> Result, auth: Extension)] pub async fn decline_invite(invitation_id: i32) -> Result { + use entity::invitation::Entity as Invitation; + use entity::invitation::InvitationStatus; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -136,6 +142,7 @@ pub async fn decline_invite(invitation_id: i32) -> Result, auth: Extension)] pub async fn list_shared_friend_events() -> Result, ServerFnError> { + use entity::links::FriendEvents; use sea_orm::ModelTrait; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; diff --git a/packages/api/src/routes/groups.rs b/packages/api/src/routes/groups.rs index 708e1c10..a78f957b 100644 --- a/packages/api/src/routes/groups.rs +++ b/packages/api/src/routes/groups.rs @@ -1,8 +1,8 @@ use crate::dioxus_fullstack::NoContent; use crate::routes::users::UserInfo; +#[cfg(feature = "server")] use crate::server; use dioxus::prelude::*; -use entity::prelude::*; use serde::{Deserialize, Serialize}; #[cfg(feature = "server")] @@ -24,7 +24,7 @@ pub async fn create_group(group_name: String) -> Result Result Result, auth: Extension)] pub async fn list_groups() -> Result, ServerFnError> { + use entity::group::Entity as Group; use sea_orm::ModelTrait; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -90,7 +91,7 @@ pub async fn add_user_to_group(group_id: i32, email: String) -> Result 0).or_not_found(format!( - "Failed to remove user {user_id} from group {group_id}" - ))?; + (result.rows_affected > 0).or_not_found("Failed to remove user from group")?; Ok(NoContent) } else { @@ -165,6 +164,9 @@ pub struct GroupDetailData { ///returns default struct of GroupDetailData when trying to call a group which does not exist #[get("/api/groups/{group_id}", ext: Extension)] pub async fn retrieve_group(group_id: i32) -> Result { + use entity::event::Entity as Event; + use entity::group::Entity as Group; + use entity::user::Entity as User; use sea_orm::EntityTrait; use sea_orm::ModelTrait; @@ -210,7 +212,7 @@ pub async fn change_group_name( let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; is_user_in_group(&ext.database, group_id, user.id) .await? - .or_forbidden("user is not part of this group")?; + .or_forbidden("User is not part of this group")?; let group = Group::find_by_id(group_id) .one(&ext.database) diff --git a/packages/api/src/routes/todo_list.rs b/packages/api/src/routes/todo_list.rs index 9f833236..f953b46c 100644 --- a/packages/api/src/routes/todo_list.rs +++ b/packages/api/src/routes/todo_list.rs @@ -1,9 +1,9 @@ +#[cfg(feature = "server")] use crate::server; use dioxus::fullstack::NoContent; use dioxus::prelude::*; #[cfg(feature = "server")] use dioxus::server::axum::Extension; -use entity::prelude::*; use entity::todo_list::{CreateTodoList, UpdateTodoList}; pub mod invite; @@ -12,7 +12,9 @@ pub mod invite; pub async fn list_todo_lists() -> Result, ServerFnError> { use entity::todo_list::Column as TodoListColumn; + use entity::todo_list::Entity as TodoList; use entity::todo_list_invitation::Column as InvitationColumn; + use entity::todo_list_invitation::Entity as TodoListInvitation; use sea_orm::ColumnTrait; use sea_orm::Condition; use sea_orm::EntityTrait; @@ -42,7 +44,9 @@ pub async fn list_todo_lists() pub async fn retrieve_todo_list( todo_list_id: i32, ) -> Result { + use entity::todo_list::Entity as TodoList; use entity::todo_list_invitation::Column as InvitationColumn; + use entity::todo_list_invitation::Entity as TodoListInvitation; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -64,6 +68,7 @@ pub async fn retrieve_todo_list( pub async fn list_todo_list_members( todo_list_id: i32, ) -> Result, ServerFnError> { + use entity::user::Entity as User; use sea_orm::EntityTrait; use sea_orm::JoinType; use sea_orm::{ColumnTrait, QueryFilter, QuerySelect, RelationTrait}; @@ -175,6 +180,7 @@ pub async fn update_todo_list( #[delete("/api/todolists/{todo_list_id}", state: Extension, auth: Extension)] pub async fn delete_todo_list(todo_list_id: i32) -> Result { + use entity::todo_list::Entity as TodoList; use sea_orm::EntityTrait; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; diff --git a/packages/api/src/routes/todo_list/invite.rs b/packages/api/src/routes/todo_list/invite.rs index 740c9e25..cbdfc1b2 100644 --- a/packages/api/src/routes/todo_list/invite.rs +++ b/packages/api/src/routes/todo_list/invite.rs @@ -1,9 +1,9 @@ +#[cfg(feature = "server")] use crate::server; use dioxus::fullstack::NoContent; use dioxus::prelude::*; #[cfg(feature = "server")] use dioxus::server::axum::Extension; -use entity::prelude::*; use entity::todo_list_invitation::{UpdateMyTodoListInvitation, UpdateTodoListInvitation}; use entity::user::UserWithTodoListInvitation; use serde::{Deserialize, Serialize}; @@ -21,6 +21,9 @@ pub async fn invite_to_todo_list( data: InviteToTodoListData, ) -> Result { use crate::routes::users::EMAIL_REGEX; + use entity::todo_list::Entity as TodoList; + use entity::todo_list_invitation::Entity as TodoListInvitation; + use entity::user::Entity as User; use regex::Regex; use sea_orm::ColumnTrait; use sea_orm::EntityTrait; @@ -124,7 +127,9 @@ pub async fn decline_todo_list_invite(todo_list_id: i32) -> Result, auth: Extension )] pub async fn leave_todo_list(todo_list_id: i32) -> Result { + use entity::todo_list::Entity as TodoList; use entity::todo_list_invitation::Column as InviteColum; + use entity::todo_list_invitation::Entity as TodoListInvitation; use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -199,6 +204,8 @@ pub async fn update_todo_list_invitation( data: UpdateTodoListInvitation, ) -> Result { use entity::todo_list_invitation::Column as InviteColum; + use entity::todo_list_invitation::Entity as TodoListInvitation; + use entity::user::Entity as User; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, @@ -265,6 +272,8 @@ pub async fn update_my_todo_list_invitation( data: UpdateMyTodoListInvitation, ) -> Result { use entity::todo_list_invitation::Column as InviteColum; + use entity::todo_list_invitation::Entity as TodoListInvitation; + use entity::user::Entity as User; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, RelationTrait, @@ -314,6 +323,7 @@ pub async fn list_todo_invites( accepted: Option, ) -> Result, ServerFnError> { use entity::todo_list_invitation::Column as InviteColum; + use entity::todo_list_invitation::Entity as TodoListInvitation; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryTrait}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; diff --git a/packages/api/src/routes/todos.rs b/packages/api/src/routes/todos.rs index 744c041a..e9bf0bb6 100644 --- a/packages/api/src/routes/todos.rs +++ b/packages/api/src/routes/todos.rs @@ -1,14 +1,15 @@ +#[cfg(feature = "server")] use crate::server; use dioxus::fullstack::NoContent; use dioxus::prelude::*; #[cfg(feature = "server")] use dioxus::server::axum::Extension; -use entity::prelude::TodoList; -use entity::prelude::*; use entity::todo::{CreateToDo, UpdateToDo}; #[get("/api/todolists/{todo_list_id}/todos", state: Extension, auth: Extension)] pub async fn list_todo(todo_list_id: i32) -> Result, ServerFnError> { + use entity::todo::Entity as Todo; + use entity::todo_list::Entity as TodoList; use sea_orm::ColumnTrait; use sea_orm::EntityTrait; use sea_orm::QueryFilter; @@ -42,6 +43,7 @@ pub async fn list_todos( completed: Option, favorite: Option, ) -> Result, ServerFnError> { + use entity::todo::Entity as Todo; use sea_orm::ColumnTrait; use sea_orm::EntityTrait; use sea_orm::JoinType; @@ -85,6 +87,7 @@ pub async fn create_todo( todo_list_id: i32, data: CreateToDo, ) -> Result { + use entity::todo_list::Entity as TodoList; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; @@ -120,6 +123,7 @@ pub async fn update_todo( todo_id: i32, data: UpdateToDo, ) -> Result { + use entity::todo::Entity as Todo; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, TryIntoModel}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; let todo = Todo::find_by_id(todo_id) @@ -157,6 +161,7 @@ pub async fn update_todo( #[delete("/api/todos/{todo_id}", state: Extension, auth: Extension)] pub async fn delete_todo(todo_id: i32) -> Result { + use entity::todo::Entity as Todo; use sea_orm::{EntityTrait, ModelTrait}; let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; diff --git a/packages/api/src/routes/users.rs b/packages/api/src/routes/users.rs index f64711b8..70fbc135 100644 --- a/packages/api/src/routes/users.rs +++ b/packages/api/src/routes/users.rs @@ -1,13 +1,12 @@ use crate::dioxus_fullstack::NoContent; +#[cfg(feature = "server")] use crate::server; use dioxus::fullstack::{SetCookie, SetHeader}; use dioxus::prelude::*; -use regex::Regex; #[cfg(feature = "server")] use dioxus::server::axum::Extension; use serde::{Deserialize, Serialize}; -use time::format_description::well_known::Rfc2822; #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct UserInfo { @@ -78,6 +77,7 @@ pub async fn sign_up( #[post("/api/users/login", ext: Extension, auth: Extension)] pub async fn login(email: String, password: String) -> Result, ServerFnError> { use crate::server::auth::{create_session, verify_user}; + use time::format_description::well_known::Rfc2822; if auth.is_authenticated() { return Err(ServerFnError::ServerError { @@ -149,6 +149,7 @@ pub async fn change_user_info( email: String, ) -> dioxus::Result { use entity::user::Entity as User; + use regex::Regex; use sea_orm::{EntityTrait, IntoActiveModel}; let email = email.trim().to_lowercase(); diff --git a/packages/api/src/server/auth.rs b/packages/api/src/server/auth.rs index 3a2fb7a0..2cefd00b 100644 --- a/packages/api/src/server/auth.rs +++ b/packages/api/src/server/auth.rs @@ -17,6 +17,7 @@ use sea_orm::{ }; use time::{Duration, OffsetDateTime}; +/// Hashes a password using Argon2 pub fn hash_password(user_password: String) -> Result { let salt = SaltString::generate(&mut OsRng); match Argon2::default().hash_password(user_password.as_bytes(), &salt) { @@ -29,6 +30,7 @@ pub fn hash_password(user_password: String) -> Result { } } +/// Verifies a password against a hash pub fn verify_password(user_password: &str, password_hashed: &str) -> bool { let parsed_hash = PasswordHash::new(password_hashed); if let Ok(hash) = parsed_hash { @@ -39,6 +41,14 @@ pub fn verify_password(user_password: &str, password_hashed: &str) -> bool { false } +/// Verifies the user credentials +/// +/// # Arguments +/// * `user_password`: Plain text password to verify +/// * `user_email`: Email of the user to verify +/// * `db`: Connection to the database +/// +/// returns: Result - Returns the user model if the credentials are correct or an error pub async fn verify_user( user_password: &str, user_email: &str, diff --git a/packages/frontend/src/components/ui/events/eventlist.rs b/packages/frontend/src/components/ui/events/eventlist.rs index 5bb2191d..88bff8c7 100644 --- a/packages/frontend/src/components/ui/events/eventlist.rs +++ b/packages/frontend/src/components/ui/events/eventlist.rs @@ -5,6 +5,7 @@ use api::routes::events::invitations::list_shared_friend_events; use api::routes::events::{delete_event, leave_event, list_events, remove_event_from_group}; use api::routes::groups::retrieve_group; use dioxus::prelude::*; +use roommates::message_from_captured_error; use time::Date; #[component] @@ -19,11 +20,16 @@ pub fn EventList(date: Option) -> Element { toaster.success("Deleted event successfully!", ToastOptions::new()); events.write().retain(|event| event.id != event_id); } - Some(Err(_)) => { - toaster.error("Failed to delete event!", ToastOptions::new()); + Some(Err(error)) => { + toaster.error( + "Failed to delete event!", + ToastOptions::new().description(rsx! { + span { {message_from_captured_error(&error)} } + }), + ); } None => { - warn!("Request did not finish!"); + warn!("Deleting event did not finish yet!"); } } }; @@ -49,14 +55,19 @@ pub fn EventListGroups(group_id: i32) -> Element { remove_event_from_group.call(event_id, group_id).await; match remove_event_from_group.value() { Some(Ok(_)) => { - toaster.success("Deleted event successfully!", ToastOptions::new()); + toaster.success("Removed event successfully!", ToastOptions::new()); group.write().events.retain(|event| event.id != event_id); } - Some(Err(_)) => { - toaster.error("Failed to delete event!", ToastOptions::new()); + Some(Err(error)) => { + toaster.error( + "Failed to remove event from group!", + ToastOptions::new().description(rsx! { + span { {message_from_captured_error(&error)} } + }), + ); } None => { - warn!("Request did not finish!"); + warn!("Removing event from group did not finish yet!"); } } }; @@ -90,11 +101,16 @@ pub fn SharedEventList() -> Element { toaster.success("Left Event!", ToastOptions::new()); shared_events.write().retain(|event| event.id != event_id); } - Some(Err(_)) => { - toaster.error("Failed to leave event!", ToastOptions::new()); + Some(Err(error)) => { + toaster.error( + "Failed to leave event!", + ToastOptions::new().description(rsx! { + span { {message_from_captured_error(&error)} } + }), + ); } None => { - warn!("Request did not finish!"); + warn!("Leaving the event did not finish yet!"); } } }; diff --git a/packages/frontend/src/views/groups/group_detailed.rs b/packages/frontend/src/views/groups/group_detailed.rs index 3d5623a5..145f2eb6 100644 --- a/packages/frontend/src/views/groups/group_detailed.rs +++ b/packages/frontend/src/views/groups/group_detailed.rs @@ -21,6 +21,7 @@ use form_hooks::use_form::{use_form, use_on_submit}; use form_hooks::use_form_field::use_form_field; use form_hooks::validators; use regex::Regex; +use roommates::message_from_captured_error; #[derive(serde::Deserialize)] struct AddUserFormData { @@ -66,12 +67,12 @@ pub fn EditGroup(group_id: i32) -> Element { toaster.error( "Failed to change group name!", ToastOptions::new().description(rsx! { - span { "{error.to_string()}" } + span { {message_from_captured_error(&error)} } }), ); } None => { - warn! {"No value present!"} + warn! {"Changing group name did not finish yet!"} } } }); @@ -223,7 +224,7 @@ pub fn GroupListEntry( Some(Ok(_)) => { toaster .success( - &format!("Removed {} successfully!", name), + &format!("Removed {} successfully from group!", name), ToastOptions::new(), ); onmemberremove.call(member_id); @@ -231,13 +232,13 @@ pub fn GroupListEntry( Some(Err(error)) => { toaster .error( - &format!("Failed to remove {}!", name), + &format!("Failed to remove {} from group!", name), ToastOptions::new().description(rsx! { - span { "{error.to_string()}" } + span { {message_from_captured_error(&error)} } }), ); } - None => warn!("Remove member did not finish!"), + None => warn!("Remove member did not finish yet!"), } } }, @@ -284,12 +285,12 @@ pub fn AddMemberForm(group_id: i32, onmemberadd: EventHandler) -> Element { toaster.error( "Failed to add user!", ToastOptions::new().description(rsx! { - span { "{error.to_string()}" } + span { {message_from_captured_error(&error)} } }), ); } None => { - warn! {"Add user did not finish"} + warn! {"Adding user to group did not finish yet!"} } } }); @@ -343,13 +344,13 @@ pub fn DeleteGroup(group_id: i32) -> Element { if let Some(Err(error)) = delete_group.value() { toaster .error( - "Deleting group failed", + "Deleting group failed!", ToastOptions::new().description(rsx! { - span { "{error.to_string()}" } + span { {message_from_captured_error(&error)} } }), ); } else { - toaster.success("Deleted group", ToastOptions::new()); + toaster.success("Deleted group successfully!", ToastOptions::new()); nav.push(Route::GroupView {}); } }, diff --git a/packages/frontend/src/views/groups/group_view.rs b/packages/frontend/src/views/groups/group_view.rs index 14e9ae2c..221e688a 100644 --- a/packages/frontend/src/views/groups/group_view.rs +++ b/packages/frontend/src/views/groups/group_view.rs @@ -14,6 +14,7 @@ use dioxus_free_icons::icons::ld_icons::{LdPlus, LdUsers}; use form_hooks::use_form::{use_form, use_on_submit}; use form_hooks::use_form_field::use_form_field; use form_hooks::validators; +use roommates::message_from_captured_error; #[derive(serde::Deserialize)] struct NewGroupName { @@ -85,14 +86,14 @@ pub fn NewGroupForm() -> Element { } Some(Err(error)) => { toaster.error( - "Failed to create group", + "Failed to create group!", ToastOptions::new().description(rsx! { - span { "{error.to_string()}" } + span { {message_from_captured_error(&error)} } }), ); } None => { - warn! {"No value present!"} + warn! {"Creating group did not finish yet!"} } } }); From 15245673408adf57a75543c3e347cd3637b57568 Mon Sep 17 00:00:00 2001 From: Mara-161 Date: Sun, 1 Mar 2026 16:31:09 +0100 Subject: [PATCH 2/3] feat: added leave group button --- packages/api/src/routes/groups.rs | 6 + .../src/components/ui/events/eventlist.rs | 14 ++- .../components/ui/events/eventlistentry.rs | 18 +-- .../frontend/src/components/ui/groupcard.rs | 3 +- .../src/views/groups/group_detailed.rs | 105 ++++++++++++++---- .../frontend/src/views/groups/group_view.rs | 29 ++--- 6 files changed, 125 insertions(+), 50 deletions(-) diff --git a/packages/api/src/routes/groups.rs b/packages/api/src/routes/groups.rs index a78f957b..4b2b4a21 100644 --- a/packages/api/src/routes/groups.rs +++ b/packages/api/src/routes/groups.rs @@ -154,6 +154,12 @@ pub async fn remove_user_from_group( } } +#[post("/api/groups/{group_id}/leave-group", auth: Extension)] +pub async fn leave_group(group_id: i32) -> Result { + let user = auth.user.as_ref().or_unauthorized("Not authenticated")?; + remove_user_from_group(group_id, user.id).await +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct GroupDetailData { pub name: String, diff --git a/packages/frontend/src/components/ui/events/eventlist.rs b/packages/frontend/src/components/ui/events/eventlist.rs index 88bff8c7..5ee42eaf 100644 --- a/packages/frontend/src/components/ui/events/eventlist.rs +++ b/packages/frontend/src/components/ui/events/eventlist.rs @@ -1,8 +1,12 @@ -use crate::components::ui::events::eventlistentry::{EventListEntry, SharedEventRow}; -use crate::components::ui::list::List; -use crate::components::ui::toaster::{ToastOptions, use_toaster}; -use api::routes::events::invitations::list_shared_friend_events; -use api::routes::events::{delete_event, leave_event, list_events, remove_event_from_group}; +use crate::components::ui::{ + events::eventlistentry::{EventListEntry, SharedEventRow}, + list::List, + toaster::{ToastOptions, use_toaster}, +}; +use api::routes::events::{ + invitations::list_shared_friend_events, + {delete_event, leave_event, list_events, remove_event_from_group}, +}; use api::routes::groups::retrieve_group; use dioxus::prelude::*; use roommates::message_from_captured_error; diff --git a/packages/frontend/src/components/ui/events/eventlistentry.rs b/packages/frontend/src/components/ui/events/eventlistentry.rs index 64f7659d..871a4b3b 100644 --- a/packages/frontend/src/components/ui/events/eventlistentry.rs +++ b/packages/frontend/src/components/ui/events/eventlistentry.rs @@ -1,9 +1,9 @@ use crate::Route; use crate::components::tooltip::Tooltip; -use crate::components::ui::form::submit_button::SubmitButton; use crate::components::ui::{ button::{Button, ButtonVariant}, dialog::{Dialog, DialogAction, DialogContent, DialogTrigger, use_dialog}, + form::submit_button::SubmitButton, form::vectorselect::VectorSelect, list::{ComplexListDetails, List, ListRow}, toaster::{ToastOptions, use_toaster}, @@ -11,13 +11,17 @@ use crate::components::ui::{ use api::routes::events::{add_event_to_group, list_event_groups, list_event_members}; use api::routes::groups::list_groups; use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::ld_icons::{ - LdEye, LdEyeOff, LdLogOut, LdMapPin, LdRefreshCcw, LdTrash, LdUserPlus, LdUsers, +use dioxus_free_icons::{ + Icon, + icons::ld_icons::{ + LdEye, LdEyeOff, LdLogOut, LdMapPin, LdRefreshCcw, LdTrash, LdUserPlus, LdUsers, + }, +}; +use form_hooks::{ + use_form::{use_form, use_on_submit}, + use_form_field::use_form_field, + validators, }; -use form_hooks::use_form::{use_form, use_on_submit}; -use form_hooks::use_form_field::use_form_field; -use form_hooks::validators; use roommates::message_from_captured_error; use time::macros::format_description; diff --git a/packages/frontend/src/components/ui/groupcard.rs b/packages/frontend/src/components/ui/groupcard.rs index cb6030f7..2de428dd 100644 --- a/packages/frontend/src/components/ui/groupcard.rs +++ b/packages/frontend/src/components/ui/groupcard.rs @@ -2,8 +2,7 @@ use crate::Route; use crate::components::ui::card::{Card, CardBody, CardTitle}; use api::routes::groups::retrieve_group; use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::ld_icons::LdSquarePen; +use dioxus_free_icons::{Icon, icons::ld_icons::LdSquarePen}; #[component] pub fn GroupCard(group_id: i32) -> Element { diff --git a/packages/frontend/src/views/groups/group_detailed.rs b/packages/frontend/src/views/groups/group_detailed.rs index 145f2eb6..d609b28e 100644 --- a/packages/frontend/src/views/groups/group_detailed.rs +++ b/packages/frontend/src/views/groups/group_detailed.rs @@ -1,25 +1,31 @@ use crate::Route; -use crate::components::ui::button::{Button, ButtonShape, ButtonVariant}; -use crate::components::ui::card::{Card, CardActions, CardBody, CardTitle}; -use crate::components::ui::dialog::{ - Dialog, DialogAction, DialogContent, DialogTrigger, use_dialog, +use crate::components::ui::{ + button::{Button, ButtonShape, ButtonVariant}, + card::{Card, CardActions, CardBody, CardTitle}, + dialog::{Dialog, DialogAction, DialogContent, DialogTrigger, use_dialog}, + events::eventlist::EventListGroups, + form::input::Input, + form::submit_button::SubmitButton, + list::{ComplexList, ListDetails, ListRow}, + toaster::{ToastOptions, use_toaster}, }; -use crate::components::ui::events::eventlist::EventListGroups; -use crate::components::ui::form::input::Input; -use crate::components::ui::form::submit_button::SubmitButton; -use crate::components::ui::list::{ComplexList, ListDetails, ListRow}; -use crate::components::ui::toaster::{ToastOptions, use_toaster}; -use api::routes::groups::retrieve_group; -use api::routes::groups::{ - add_user_to_group, change_group_name, delete_group, remove_user_from_group, +use api::routes::{ + groups::{ + add_user_to_group, change_group_name, delete_group, leave_group, remove_user_from_group, + retrieve_group, + }, + users::{EMAIL_REGEX, UserInfo}, }; -use api::routes::users::{EMAIL_REGEX, UserInfo}; use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::ld_icons::{LdMail, LdMinus, LdPlus, LdUsers}; -use form_hooks::use_form::{use_form, use_on_submit}; -use form_hooks::use_form_field::use_form_field; -use form_hooks::validators; +use dioxus_free_icons::{ + Icon, + icons::ld_icons::{LdLogOut, LdMail, LdMinus, LdPlus, LdTrash, LdUsers}, +}; +use form_hooks::{ + use_form::{use_form, use_on_submit}, + use_form_field::use_form_field, + validators, +}; use regex::Regex; use roommates::message_from_captured_error; @@ -133,7 +139,7 @@ pub fn EditGroup(group_id: i32) -> Element { } } } - div { class: "relative flex flex-col md:w-1/6 overflow-y-auto w-full", + div { class: "relative flex flex-col md:w-1/6 overflow-y-auto w-full pb-32", ComplexList { header: rsx! { div { class: "flex justify-between items-center w-full", @@ -171,19 +177,31 @@ pub fn EditGroup(group_id: i32) -> Element { } } } - div { + div { class: "fixed bottom-16 lg:bottom-4 right-4 flex gap-3", Dialog { DialogTrigger { - variant: ButtonVariant::Primary, + variant: ButtonVariant::Error, shape: ButtonShape::Default, ghost: false, - class: "fixed bottom-16 lg:bottom-4 right-4 btn btn-primary lg:btn-lg", - "Delete group" + class: "btn lg:btn-lg", + Icon { icon: LdTrash } } DialogContent { title: "Do you want to delete this group?", DeleteGroup { group_id } } } + Dialog { + DialogTrigger { + variant: ButtonVariant::Error, + shape: ButtonShape::Default, + ghost: false, + class: "btn lg:btn-lg", + Icon { icon: LdLogOut } + } + DialogContent { title: "Do you want to leave this group?", + LeaveGroup { group_id } + } + } } } } @@ -361,3 +379,44 @@ pub fn DeleteGroup(group_id: i32) -> Element { } } } + +#[component] +pub fn LeaveGroup(group_id: i32) -> Element { + let mut toaster = use_toaster(); + let dialog = use_dialog(); + let mut leave_group = use_action(leave_group); + let nav = navigator(); + + rsx! { + DialogAction { + Button { + onclick: move |_| { + dialog.close(); + }, + r#type: "button", + variant: ButtonVariant::Secondary, + "Cancel" + } + Button { + onclick: move |_| async move { + leave_group.call(group_id).await; + if let Some(Err(error)) = leave_group.value() { + toaster + .error( + "Leaving group failed!", + ToastOptions::new().description(rsx! { + span { {message_from_captured_error(&error)} } + }), + ); + } else { + toaster.success("Left group successfully!", ToastOptions::new()); + nav.push(Route::GroupView {}); + } + }, + r#type: "submit", + variant: ButtonVariant::Primary, + "Leave" + } + } + } +} diff --git a/packages/frontend/src/views/groups/group_view.rs b/packages/frontend/src/views/groups/group_view.rs index 221e688a..688c263b 100644 --- a/packages/frontend/src/views/groups/group_view.rs +++ b/packages/frontend/src/views/groups/group_view.rs @@ -1,19 +1,22 @@ use crate::Route; -use crate::components::ui::button::{Button, ButtonShape, ButtonVariant}; -use crate::components::ui::dialog::{ - Dialog, DialogAction, DialogContent, DialogTrigger, use_dialog, +use crate::components::ui::{ + button::{Button, ButtonShape, ButtonVariant}, + dialog::{Dialog, DialogAction, DialogContent, DialogTrigger, use_dialog}, + form::input::Input, + groupcard::GroupCard, + toaster::{ToastOptions, use_toaster}, }; -use crate::components::ui::form::input::Input; -use crate::components::ui::groupcard::GroupCard; -use crate::components::ui::toaster::{ToastOptions, use_toaster}; -use api::routes::groups::create_group; -use api::routes::groups::list_groups; +use api::routes::groups::{create_group, list_groups}; use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::ld_icons::{LdPlus, LdUsers}; -use form_hooks::use_form::{use_form, use_on_submit}; -use form_hooks::use_form_field::use_form_field; -use form_hooks::validators; +use dioxus_free_icons::{ + Icon, + icons::ld_icons::{LdPlus, LdUsers}, +}; +use form_hooks::{ + use_form::{use_form, use_on_submit}, + use_form_field::use_form_field, + validators, +}; use roommates::message_from_captured_error; #[derive(serde::Deserialize)] From a54e527191f9dc30c87e5fc0e262804896515469 Mon Sep 17 00:00:00 2001 From: Mara-161 Date: Sun, 1 Mar 2026 17:50:21 +0100 Subject: [PATCH 3/3] fix: error message --- packages/api/src/routes/groups.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/routes/groups.rs b/packages/api/src/routes/groups.rs index 4b2b4a21..d27261b9 100644 --- a/packages/api/src/routes/groups.rs +++ b/packages/api/src/routes/groups.rs @@ -24,7 +24,7 @@ pub async fn create_group(group_name: String) -> Result