diff --git a/Cargo.lock b/Cargo.lock index d1c8b64..4923757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3064,6 +3064,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sorted-vec" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0" + [[package]] name = "spin" version = "0.9.8" @@ -4053,6 +4059,7 @@ dependencies = [ "rand 0.9.2", "reactive_stores", "serde", + "sorted-vec", "sqlx", "tokio", "tower-http", diff --git a/Cargo.toml b/Cargo.toml index 7cb7101..7ed3228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,7 @@ ciborium = { version = "0.2" } pretty_assertions = { version = "1.4" } colored = { version = "3.1" } pretty_env_logger = { version = "0.5" } +sorted-vec = { version = "0.8" } [profile.dev] opt-level = 0 diff --git a/style/main.scss b/style/main.scss index 18ea6a7..d6803c7 100644 --- a/style/main.scss +++ b/style/main.scss @@ -241,6 +241,10 @@ dialog::backdrop { &.full { border-bottom: 1px solid white; } + + .headline { + user-select: none; + } } } @@ -281,26 +285,7 @@ dialog::backdrop { max-width: 70%; } - form { - font-size: 1em; - display: flex; - flex-direction: column; - gap: 1ch; - align-items: center; - .form-fields { - display: flex; - flex-direction: column; - align-items: center; - // width: 100%; - gap: 1ch; - } - - - label { - user-select: none; - } - } } .user-settings-list { @@ -659,13 +644,24 @@ dialog .tab-content { } } +.host-seat-changer { + width: min-content; + + input[type=submit] { + width: 100%; + } +} + .number-update:not([hidden]) { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 0.25ch; margin-top: 3ch; - font-size: 1.5em; + + .bigger { + font-size: 1.5em; + } input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { @@ -756,3 +752,35 @@ dialog .tab-content { border: 1px solid red; background-color: black; } + +.player-options { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + max-width: 80vw; + gap: 0.25ch; + min-width: 45vw; + + &>* { + width: 100%; + } +} + +form { + font-size: 1em; + display: flex; + flex-direction: column; + gap: 0.25ch; + align-items: center; + + .form-fields { + display: flex; + flex-direction: column; + align-items: center; + gap: 1ch; + } + + label { + user-select: none; + } +} diff --git a/werewolves-proto/src/message/dead.rs b/werewolves-proto/src/message/dead.rs index 8c3870a..9f63a58 100644 --- a/werewolves-proto/src/message/dead.rs +++ b/werewolves-proto/src/message/dead.rs @@ -13,6 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use core::hash::Hash; use std::collections::HashMap; use chrono::{DateTime, TimeDelta, Utc}; @@ -204,6 +205,24 @@ pub struct DeadChatMessage { pub message: DeadChatContent, } +impl Hash for DeadChatMessage { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl Ord for DeadChatMessage { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for DeadChatMessage { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DeadChatContent { PlayerMessage { diff --git a/werewolves/Cargo.toml b/werewolves/Cargo.toml index 72cc22d..60e9ab1 100644 --- a/werewolves/Cargo.toml +++ b/werewolves/Cargo.toml @@ -40,6 +40,7 @@ werewolves-macros.workspace = true werewolves-proto.workspace = true codee.workspace = true convert_case.workspace = true +sorted-vec.workspace = true [features] hydrate = [ diff --git a/werewolves/src/app/components/input/number.rs b/werewolves/src/app/components/input/number.rs new file mode 100644 index 0000000..48a2d1e --- /dev/null +++ b/werewolves/src/app/components/input/number.rs @@ -0,0 +1,75 @@ +use core::num::NonZeroU8; + +use leptos::{ + ev::{Event, MouseEvent, SubmitEvent, Targeted}, + html::Form, + prelude::*, + tachys::html::node_ref::NodeRefContainer, + web_sys::HtmlInputElement, +}; + +use crate::ConsoleLogError; + +#[component] +pub fn ChangePlayerNumber( + submitted_number: WriteSignal>, + error: WriteSignal>, +) -> impl IntoView { + let number: RwSignal> = RwSignal::new(None); + let update = move |e: Targeted| { + e.prevent_default(); + let value = e.target().value(); + if value.trim().is_empty() { + number.set(None); + return; + } + let current = number.get_untracked(); + let default = current.map(|c| c.get().to_string()).unwrap_or_default(); + let value_u8 = match e.target().value().trim().parse::() { + Ok(v) => v, + Err(err) => { + log::error!("{err}"); + e.target().set_value(default.as_str()); + return; + } + }; + if let Some(nz) = NonZeroU8::new(value_u8) { + number.set(Some(nz)); + } else { + e.target().set_value(default.as_str()); + } + }; + let submit = move |ev: SubmitEvent| { + ev.prevent_default(); + log::warn!("called submit with number: {:?}", number.get()); + let Some(num) = number.get() else { + error.set(Some("please set a number".into())); + return; + }; + submitted_number.set(Some(num)); + }; + let submit_click = move |ev: MouseEvent| { + ev.prevent_default(); + log::warn!("called submit with number: {:?}", number.get()); + let Some(num) = number.get() else { + error.set(Some("please set a number".into())); + return; + }; + submitted_number.set(Some(num)); + }; + move || { + view! { + + "change seat number" + + + + } + } +} diff --git a/werewolves/src/app/components/modal.rs b/werewolves/src/app/components/modal.rs index 41c78b5..01312b1 100644 --- a/werewolves/src/app/components/modal.rs +++ b/werewolves/src/app/components/modal.rs @@ -13,7 +13,7 @@ pub enum DialogMode { #[component] pub fn DialogModal( - text: String, + button_content: impl IntoView + Clone, #[prop(optional)] open: Option>, #[prop(optional)] button_class: String, #[prop(default = Box::new(|| ().into_any()))] mut children: ChildrenFnMut, @@ -23,15 +23,13 @@ pub fn DialogModal( // use generated id if not supplied let open = open.unwrap_or_else(|| RwSignal::new(false)); - let close_cb = { - move |ev: MouseEvent| { - ev.prevent_default(); - open.set(false); - } + let close_cb = move |ev: MouseEvent| { + ev.prevent_default(); + open.set(false); }; let close = { view! { - + "close" } @@ -48,34 +46,31 @@ pub fn DialogModal( .into_any(), }; let dialog_element: NodeRef = NodeRef::new(); - let on_backdrop_click = { - let close_cb = close_cb.clone(); - move |ev: MouseEvent| { - ev.prevent_default(); - if !close_backdrop { + let on_backdrop_click = move |ev: MouseEvent| { + ev.prevent_default(); + if !close_backdrop { + return; + } + #[cfg(feature = "hydrate")] + { + let Some(dialog) = dialog_element.get() else { + log::error!("dialog_element is None"); return; - } - #[cfg(feature = "hydrate")] - { - let Some(dialog) = dialog_element.get() else { - log::error!("dialog_element is None"); - return; - }; + }; - let Ok(Some(dialog_box)) = dialog.query_selector(".dialog-box") else { - log::error!(".dialog-box is None"); - return; - }; - let rect: leptos::web_sys::DomRect = dialog_box.get_bounding_client_rect(); + let Ok(Some(dialog_box)) = dialog.query_selector(".dialog-box") else { + log::error!(".dialog-box is None"); + return; + }; + let rect: leptos::web_sys::DomRect = dialog_box.get_bounding_client_rect(); - let is_in_dialog = rect.top() as i32 <= ev.client_y() - && ev.client_y() <= rect.top() as i32 + rect.height() as i32 - && rect.left() as i32 <= ev.client_x() - && ev.client_x() <= rect.left() as i32 + rect.width() as i32; + let is_in_dialog = rect.top() as i32 <= ev.client_y() + && ev.client_y() <= rect.top() as i32 + rect.height() as i32 + && rect.left() as i32 <= ev.client_x() + && ev.client_x() <= rect.left() as i32 + rect.width() as i32; - if !is_in_dialog { - close_cb(ev); - } + if !is_in_dialog { + close_cb(ev); } } }; @@ -97,11 +92,13 @@ pub fn DialogModal( ev.prevent_default(); open.set(true); }; - let button = text.is_empty().not().then_some(view! { - - {text.clone()} - - }); + let button = { + view! { + + {button_content.clone().into_view()} + + } + }; view! { {button} {modal} } } diff --git a/werewolves/src/app/pages/game/host.rs b/werewolves/src/app/pages/game/host.rs index fc919c8..f9b5971 100644 --- a/werewolves/src/app/pages/game/host.rs +++ b/werewolves/src/app/pages/game/host.rs @@ -88,6 +88,7 @@ pub fn HostGamePage( HostPage::None => ().into_any(), HostPage::Settings => view! { "this will cancel the game. are you sure?" diff --git a/werewolves/src/app/pages/game/host/players.rs b/werewolves/src/app/pages/game/host/players.rs index eb1bcf0..1dea94b 100644 --- a/werewolves/src/app/pages/game/host/players.rs +++ b/werewolves/src/app/pages/game/host/players.rs @@ -1,19 +1,49 @@ -use leptos::prelude::*; -use werewolves_proto::message::{Identification, PlayerState}; +use core::num::NonZeroU8; -use crate::app::components::{Equals, IdentificationInline, Sample, TutorialBox}; +use leptos::prelude::*; +use werewolves_proto::message::{ + Identification, PlayerState, + host::{HostLobbyMessage, HostMessage}, +}; + +use crate::app::{ + class::AsClasses, + components::{ + DialogModal, DialogMode, Equals, IdentificationInline, Sample, TutorialBox, + input::ChangePlayerNumber, + }, +}; #[component] -pub fn HostPlayerList(players: ReadSignal>) -> impl IntoView { +pub fn HostPlayerList( + players: ReadSignal>, + reply: WriteSignal>, + error: WriteSignal>, +) -> impl IntoView { let players = move || { + let mut players = players.get(); + players.sort_by_key(|p| p.identification.public.number.unwrap_or(NonZeroU8::MAX)); players - .read() - .iter() + .into_iter() .map(|p| { + let ident = p.identification.clone(); + let button_content = move || { + view! { } + }; + let connected = if p.connected { + "connected" + } else { + Default::default() + }; view! { - - - + + + } }) .collect_view() @@ -53,3 +83,46 @@ pub fn HostPlayerList(players: ReadSignal>) -> impl IntoView } } + +#[component] +fn HostPlayerDialogBody( + player: PlayerState, + reply: WriteSignal>, + error: WriteSignal>, +) -> impl IntoView { + let pid = player.identification.player_id; + let kick = move |_| reply.set(Some(HostMessage::Lobby(HostLobbyMessage::Kick(pid)))); + let set_number = RwSignal::new(None); + let num_change_open = RwSignal::new(false); + let num_change_button_text = move || { + if num_change_open.get() { + "close seat changer" + } else { + "change seat number" + } + }; + Effect::new(move || { + if let Some(num) = set_number.get() { + log::info!("gunna set seat number to {num}"); + reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber( + pid, num, + )))); + } + }); + let toggle_change_seat_open = move |_| num_change_open.set(!num_change_open.get()); + move || { + view! { + + + + + + "kick" + {num_change_button_text} + + + + + } + } +} diff --git a/werewolves/src/app/pages/game/host/settings.rs b/werewolves/src/app/pages/game/host/settings.rs index 714d0f6..fd2fd12 100644 --- a/werewolves/src/app/pages/game/host/settings.rs +++ b/werewolves/src/app/pages/game/host/settings.rs @@ -27,6 +27,7 @@ pub fn Settings( dialog_open: RwSignal>, open_categories: RwSignal>, reply: WriteSignal>, + error: WriteSignal>, ) -> impl IntoView { let slots = move || { settings @@ -129,7 +130,7 @@ pub fn Settings( {categories} {slots} - + } } #[component] @@ -211,7 +212,7 @@ fn SettingsSetupSlot( ] .as_classes() .to_string() - text=setup_slot.read().role.title().to_string().to_case(Case::Title) + button_content=setup_slot.read().role.title().to_string().to_case(Case::Title) close_backdrop=true > diff --git a/werewolves/src/app/pages/game/player.rs b/werewolves/src/app/pages/game/player.rs index 530656b..34ca888 100644 --- a/werewolves/src/app/pages/game/player.rs +++ b/werewolves/src/app/pages/game/player.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use convert_case::{Case, Casing}; use leptos::prelude::*; +use sorted_vec::SortedSet; use werewolves_proto::{ message::{ ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage, @@ -35,8 +36,7 @@ pub fn PlayerGamePage( reply: WriteSignal>, disconnect: RwSignal, ) -> impl IntoView { - let error = RwSignal::new(None); - let dead_chat: RwSignal>> = RwSignal::new(None); + let dead_chat: RwSignal>> = RwSignal::new(None); let page: RwSignal> = RwSignal::new(None); Effect::new(move || { let Some(message) = message.get() else { @@ -64,17 +64,12 @@ pub fn PlayerGamePage( Srv2Client::Story(game_story) => todo!("{game_story:#?}"), Srv2Client::Update(_) => {} Srv2Client::DeadChat(dead_chat_messages) => { - dead_chat.set(Some( - dead_chat_messages - .into_iter() - .map(Into::::into) - .collect(), - )); + dead_chat.set(Some(dead_chat_messages.into_iter().collect())); page.set(Some(Page::DeadChat)); } Srv2Client::DeadChatMessage(dead_chat_message) => { if let Some(chat) = dead_chat.write().as_mut() { - chat.insert(dead_chat_message.into()); + chat.push(dead_chat_message); page.set(Some(Page::DeadChat)); } else { reply.set(Some(ClientMessage::DeadChat(ClientDeadChat::GetHistory))); @@ -134,45 +129,5 @@ pub fn PlayerGamePage( .into_any(), } }; - view! { - - {content} - } - .into_any() -} - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HashedDeadChatMessage(DeadChatMessage); -impl Ord for HashedDeadChatMessage { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.timestamp.cmp(&other.timestamp) - } -} -impl PartialOrd for HashedDeadChatMessage { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Hash for HashedDeadChatMessage { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} -impl From for HashedDeadChatMessage { - fn from(value: DeadChatMessage) -> Self { - Self(value) - } -} -impl From for DeadChatMessage { - fn from(value: HashedDeadChatMessage) -> Self { - value.0 - } -} -impl Deref for HashedDeadChatMessage { - type Target = DeadChatMessage; - - fn deref(&self) -> &Self::Target { - &self.0 - } + view! { {content} }.into_any() } diff --git a/werewolves/src/app/pages/game/player/lobby.rs b/werewolves/src/app/pages/game/player/lobby.rs index 0945d0a..dd29746 100644 --- a/werewolves/src/app/pages/game/player/lobby.rs +++ b/werewolves/src/app/pages/game/player/lobby.rs @@ -11,7 +11,7 @@ pub fn PlayerLobby( reply: WriteSignal>, joined: ReadSignal, current_number: ReadSignal>, - error: RwSignal>, + error: WriteSignal>, ) -> impl IntoView { let click = move |ev: MouseEvent| { ev.prevent_default(); @@ -68,7 +68,7 @@ pub fn PlayerLobby( form_hidden.set(true); }; view! { - + "change seat number" impl IntoView { }; view! { - + diff --git a/werewolves/src/app/pages/user_settings/update_profile.rs b/werewolves/src/app/pages/user_settings/update_profile.rs index 98d655f..aff8f01 100644 --- a/werewolves/src/app/pages/user_settings/update_profile.rs +++ b/werewolves/src/app/pages/user_settings/update_profile.rs @@ -81,7 +81,7 @@ pub fn UpdateProfileButton() -> impl IntoView { .then_some(view! { "profile updated" }) }; view! { - + "display name"
"profile updated"