Skip to content

Commit

Permalink
feat: impl player table editing
Browse files Browse the repository at this point in the history
  • Loading branch information
JacksonVirgo committed Aug 2, 2024
1 parent 0f6d227 commit e1e06f9
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 36 deletions.
4 changes: 4 additions & 0 deletions migrations/0001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ CREATE TABLE IF NOT EXISTS players (
FOREIGN KEY (thread_id) REFERENCES threads(thread_id) ON DELETE CASCADE
);

# Add a new element to players. replacements TEXT[] NOT NULL DEFAULT '{}

ALTER TABLE players
ADD COLUMN IF NOT EXISTS replacements TEXT[] NOT NULL DEFAULT '{}';
24 changes: 19 additions & 5 deletions src/components/forms/input/select_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ use maud::{html, Markup};
pub struct SelectMenuInput {
pub placeholder: String,
pub name: String,
pub is_required: Option<bool>,
pub is_required: bool,
pub options: Vec<String>,
pub default_value: Option<String>,
}

pub struct SelectMenuBuilder {
pub placeholder: String,
pub name: String,
pub is_required: Option<bool>,
pub is_required: bool,
pub options: Vec<String>,
pub default_value: Option<String>,
}
Expand All @@ -21,7 +21,7 @@ impl SelectMenuBuilder {
SelectMenuBuilder {
placeholder: String::new(),
name: String::new(),
is_required: None,
is_required: false,
options: Vec::new(),
default_value: None,
}
Expand Down Expand Up @@ -66,8 +66,22 @@ impl SelectMenuBuilder {
}
};

if input.is_required {
return html! {
select."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" name=(input.name) id=(input.name) required {
option value="" disabled selected {
(input.placeholder)
}

@for option in &input.options {
(add_row(option.clone()))
}
}
};
}

html! {
select."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" name=(input.name) id=(input.name) required=(input.is_required.unwrap_or(false)) {
select."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" name=(input.name) id=(input.name) {
option value="" disabled selected {
(input.placeholder)
}
Expand All @@ -90,7 +104,7 @@ impl SelectMenuBuilder {
}

pub fn is_required(mut self, is_required: bool) -> Self {
self.is_required = Some(is_required);
self.is_required = is_required;
self
}

Expand Down
27 changes: 22 additions & 5 deletions src/components/forms/input/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ use maud::{html, Markup};
pub struct TextInput {
pub placeholder: String,
pub name: String,
pub is_required: Option<bool>,
pub is_required: bool,
pub default_value: Option<String>,
pub is_hidden: bool,
}

pub struct TextInputBuilder {
pub placeholder: String,
pub name: String,
pub is_required: Option<bool>,
pub is_required: bool,
pub default_value: Option<String>,
pub is_hidden: bool,
}

impl TextInputBuilder {
pub fn new() -> TextInputBuilder {
TextInputBuilder {
placeholder: String::new(),
name: String::new(),
is_required: None,
is_required: false,
default_value: None,
is_hidden: false,
}
}

Expand All @@ -31,13 +34,27 @@ impl TextInputBuilder {
name: self.name,
is_required: self.is_required,
default_value: self.default_value,
is_hidden: self.is_hidden,
}
}

pub fn build_html(self) -> Markup {
let input = self.build();

let mut style =
"w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700".to_string();
if input.is_hidden {
style.push_str(" hidden");
}

if input.is_required {
return html! {
input.(style) type="text" name=(input.name) id=(input.name) placeholder=(input.placeholder) required value=(input.default_value.unwrap_or("".to_string())) {}
};
}

html! {
input."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" type="text" name=(input.name) id=(input.name) placeholder=(input.placeholder) required=(input.is_required.unwrap_or(false)) value=(input.default_value.unwrap_or("".to_string())) {}
input.(style) type="text" name=(input.name) id=(input.name) placeholder=(input.placeholder) value=(input.default_value.unwrap_or("".to_string())) {}
}
}

Expand All @@ -52,7 +69,7 @@ impl TextInputBuilder {
}

pub fn is_required(mut self, is_required: bool) -> Self {
self.is_required = Some(is_required);
self.is_required = is_required;
self
}

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct AppState {
db: Pool<Postgres>,
}

// TODO: Remove hardcoded static files, add to custom route
const STYLE_CSS: &[u8] = include_bytes!("./static/output.css");
const FAVICON: &[u8] = include_bytes!("./static/favicon.ico");
#[get("/style.css")]
Expand Down
69 changes: 65 additions & 4 deletions src/models/players.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::AppState;
use actix_web::web::Data;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::{self, postgres::PgQueryResult, FromRow};
use sqlx::FromRow;
use sqlx::{self, postgres::PgQueryResult};
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumString};

Expand All @@ -16,9 +17,9 @@ pub enum PlayerAlignment {
Werewolf,
#[strum(serialize = "Cult")]
Cult,
#[strum(serialize = "Self-Aligned (Killing)")]
#[strum(serialize = "SelfAlignedKilling")]
SelfAlignedKilling,
#[strum(serialize = "Self-Aligned (Other)")]
#[strum(serialize = "SelfAlignedOther")]
SelfAlignedOther,
#[strum(serialize = "Unknown")]
Unknown,
Expand All @@ -37,16 +38,27 @@ pub struct Player {
pub role: Option<String>,
pub created_at: Option<NaiveDateTime>,
pub alignment: Option<PlayerAlignment>,
pub aliases: Vec<String>,
pub replacements: Vec<String>,

// FKs
pub thread_id: Option<String>,
}

pub struct UpdatePlayer {
pub id: i32,
pub name: String,
pub role: Option<String>,
pub aliases: Vec<String>,
pub replacements: Vec<String>,
pub alignment: Option<PlayerAlignment>,
}

pub async fn get_players(app_state: &Data<AppState>, thread_id: &str) -> Option<Vec<Player>> {
let db = &app_state.db;
match sqlx::query_as!(
Player,
r#"SELECT id, name, alignment as "alignment: PlayerAlignment", role, created_at, thread_id FROM players WHERE thread_id = $1"#,
r#"SELECT id, name, alignment as "alignment: PlayerAlignment", role, aliases, replacements, created_at, thread_id FROM players WHERE thread_id = $1"#,
thread_id
)
.fetch_all(db)
Expand All @@ -57,6 +69,21 @@ pub async fn get_players(app_state: &Data<AppState>, thread_id: &str) -> Option<
}
}

pub async fn get_player(app_state: &Data<AppState>, id: i32) -> Option<Player> {
let db = &app_state.db;
match sqlx::query_as!(
Player,
r#"SELECT id, name, alignment as "alignment: PlayerAlignment", role, aliases, replacements, created_at, thread_id FROM players WHERE id = $1"#,
id
)
.fetch_one(db)
.await
{
Ok(player) => Some(player),
_ => None,
}
}

pub async fn create_player(
app_state: &Data<AppState>,
thread_id: &str,
Expand All @@ -75,3 +102,37 @@ pub async fn create_player(
_ => None,
}
}

pub async fn update_player(
app_state: &Data<AppState>,
player: UpdatePlayer,
) -> Option<PgQueryResult> {
let db = &app_state.db;

let role = match player.role {
Some(r) => {
if r.trim().is_empty() {
None
} else {
Some(r)
}
}
None => None,
};

match sqlx::query!(
r#"UPDATE players SET name = $1, alignment = ($2::text)::alignment, role = $3, aliases = $4, replacements = $5 WHERE id = $6"#,
player.name,
player.alignment.map(|a| a.to_string()), // Convert alignment to string
role,
&player.aliases,
&player.replacements,
player.id
)
.execute(db)
.await
{
Ok(player) => Some(player),
_ => None,
}
}
3 changes: 3 additions & 0 deletions src/routes/api/dashboard/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
pub mod player_data;
pub mod player_edit;
pub mod setup_data;
pub mod vote_data;

pub fn init(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(player_data::player_data);
cfg.service(player_data::add_player);
cfg.service(player_edit::player_data);
cfg.service(player_edit::player_edit);

cfg.service(vote_data::vote_data);

Expand Down
61 changes: 49 additions & 12 deletions src/routes/api/dashboard/player_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,49 @@ struct FormData {
}

struct TableRow {
id: i32,
name: String,
alignment: String,
role: String,
replacements: String,
aliases: Vec<String>,
replacements: Vec<String>,
}
fn format_table_row(row: TableRow) -> Markup {

fn format_table_row(thread_id: &str, row: TableRow) -> Markup {
let mut aliases = row.aliases.clone();
let mut replacements = row.replacements.clone();

if aliases.len() == 0 {
aliases.push("None".to_string());
}

if replacements.len() == 0 {
replacements.push("None".to_string());
}

html!({
tr."even:bg-zinc-600 hover:cursor-pointer hover:bg-zinc-500" {
tr."even:bg-zinc-600 hover:cursor-pointer hover:bg-zinc-500" hx-get=(format!("/api/dashboard/playeredit/{}/{}", thread_id, row.id)) hx-target="#player-wrapper" hx-trigger="click" {
td."px-4 py-2" { (row.name) }
td."px-4 py-2 border-l border-gray-200" { (row.alignment) }
td."px-4 py-2 border-l border-gray-200" { (row.role) }
td."px-4 py-2 border-l border-gray-200" { (row.replacements) }
td."px-4 py-2 border-l border-gray-200" {
ul."list-disc pl-2" {
@for alias in aliases {
li {
(alias)
}
}
}
}
td."px-4 py-2 border-l border-gray-200" {
ul."list-disc pl-2" {
@for replacement in replacements {
li {
(replacement)
}
}
}
}
}
})
}
Expand Down Expand Up @@ -59,15 +90,20 @@ async fn player_data(state: Data<AppState>, raw_thread_id: web::Path<String>) ->
let player_rows: Vec<Markup> = players
.iter()
.map(|p| {
format_table_row(TableRow {
name: p.name.clone(),
alignment: match p.alignment.clone() {
Some(a) => a.to_string(),
None => "Not Set".to_string(),
format_table_row(
&thread_id,
TableRow {
id: p.id,
name: p.name.clone(),
alignment: match p.alignment.clone() {
Some(a) => a.to_string(),
None => "Not Set".to_string(),
},
role: p.role.clone().unwrap_or("Not Set".to_string()),
aliases: p.aliases.clone(),
replacements: p.replacements.clone(),
},
role: p.role.clone().unwrap_or("None".to_string()),
replacements: p.role.clone().unwrap_or("None".to_string()),
})
)
})
.collect();

Expand Down Expand Up @@ -102,6 +138,7 @@ async fn player_data(state: Data<AppState>, raw_thread_id: web::Path<String>) ->
th."px-4 py-2 border-gray-200 bg-zinc-800" { "Player Name" }
th."px-4 py-2 border-l border-gray-200 bg-zinc-800" { "Alignment" }
th."px-4 py-2 border-l border-gray-200 bg-zinc-800" { "Role" }
th."px-4 py-2 border-l border-gray-200 bg-zinc-800" { "Aliases" }
th."px-4 py-2 border-l border-gray-200 bg-zinc-800" { "Replacements" }
}
}
Expand Down
Loading

0 comments on commit e1e06f9

Please sign in to comment.