From 22af8c93aa28d647d66d49e160e05d02d2841ad5 Mon Sep 17 00:00:00 2001 From: Jack Nash Date: Tue, 23 Jul 2024 18:47:52 +1000 Subject: [PATCH] feat: add dashboard page skeleton --- src/components/forms/input.rs | 2 +- src/main.rs | 1 + src/models/mod.rs | 1 + src/models/thread.rs | 45 ++++++++++++ src/routes/api/dashboard/mod.rs | 9 +++ src/routes/api/dashboard/player_data.rs | 34 +++++++++ src/routes/api/dashboard/setup_data.rs | 49 +++++++++++++ src/routes/api/dashboard/vote_data.rs | 81 +++++++++++++++++++++ src/routes/api/mod.rs | 6 +- src/routes/api/search_or_register_thread.rs | 68 +++++++++++++++++ src/routes/pages/dashboard.rs | 34 +++++++++ src/routes/pages/mod.rs | 4 +- src/routes/pages/scraper.rs | 2 +- src/routes/pages/test.rs | 42 ----------- src/scraping/parser.rs | 25 ------- src/scraping/scraper.rs | 41 +---------- 16 files changed, 331 insertions(+), 113 deletions(-) create mode 100644 src/models/mod.rs create mode 100644 src/models/thread.rs create mode 100644 src/routes/api/dashboard/mod.rs create mode 100644 src/routes/api/dashboard/player_data.rs create mode 100644 src/routes/api/dashboard/setup_data.rs create mode 100644 src/routes/api/dashboard/vote_data.rs create mode 100644 src/routes/api/search_or_register_thread.rs create mode 100644 src/routes/pages/dashboard.rs delete mode 100644 src/routes/pages/test.rs diff --git a/src/components/forms/input.rs b/src/components/forms/input.rs index 39c6685..37aefd8 100644 --- a/src/components/forms/input.rs +++ b/src/components/forms/input.rs @@ -29,7 +29,7 @@ pub fn gen_input(raw_input: InputType) -> Markup { } InputType::SelectMenuInput(input) => { html! { - select."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" name=(input.name) id=(input.name) placeholder=(input.placeholder) { + select."w-full px-4 py-2 border border-gray-300 rounded text-white bg-zinc-700" name=(input.name) id=(input.name) placeholder=(input.placeholder) required=(input.is_required.unwrap_or(false)) { @for option in &input.options { option value=(option.clone()) selected=(Some(option.clone()) == input.default_value.clone()) { (option) diff --git a/src/main.rs b/src/main.rs index 823f095..c189b55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use dotenv::dotenv; use mime; use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; mod components; +mod models; mod routes; mod scraping; diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..7c2b43b --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1 @@ +pub mod thread; diff --git a/src/models/thread.rs b/src/models/thread.rs new file mode 100644 index 0000000..813e513 --- /dev/null +++ b/src/models/thread.rs @@ -0,0 +1,45 @@ +use crate::AppState; +use actix_web::web::Data; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use sqlx::{self, FromRow}; + +#[derive(Serialize, Deserialize, FromRow, Debug)] +pub struct Thread { + id: i32, + thread_id: String, + title: Option, + queue: Option, + queue_index: Option, + created_at: Option, +} + +pub async fn get_thread(app_state: Data, thread_id: String) -> Option { + let db = &app_state.db; + match sqlx::query_as!( + Thread, + "SELECT * FROM threads WHERE thread_id = $1", + thread_id + ) + .fetch_optional(db) + .await + { + Ok(Some(thread)) => Some(thread), + _ => None, + } +} + +// pub async fn create_thread(app_state: Data, thread_id: String) -> Option { +// let db = &app_state.db; +// match sqlx::query_as!( +// Thread, +// "INSERT INTO threads (thread_id) VALUES ($1) RETURNING *", +// thread_id +// ) +// .fetch_one(db) +// .await +// { +// Ok(thread) => Some(thread), +// _ => None, +// } +// } diff --git a/src/routes/api/dashboard/mod.rs b/src/routes/api/dashboard/mod.rs new file mode 100644 index 0000000..dad3cfa --- /dev/null +++ b/src/routes/api/dashboard/mod.rs @@ -0,0 +1,9 @@ +pub mod player_data; +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(vote_data::vote_data); + cfg.service(setup_data::setup_data); +} diff --git a/src/routes/api/dashboard/player_data.rs b/src/routes/api/dashboard/player_data.rs new file mode 100644 index 0000000..b728232 --- /dev/null +++ b/src/routes/api/dashboard/player_data.rs @@ -0,0 +1,34 @@ +use crate::components::{ + buttons::{gen_button, ButtonType, FormSubmitButton}, + forms::input::{gen_input, InputType, SelectMenuInput}, +}; +use actix_web::{get, HttpResponse, Responder}; +use maud::html; + + +#[get("/players")] +async fn player_data() -> impl Responder { + HttpResponse::Ok().body( + html! { + div."w-full h-full flex flex-col p-4" { + h1."text-3xl text-white font-bold pb-2" { "Player Data" } + div."text-xl text-white pb-2" { "Enter the data for the players in the game" } + form."flex flex-col gap-2" { + label."text-xl" for="game_queue" { "Placeholder" } + (gen_input(InputType::SelectMenuInput(SelectMenuInput { + name: "game_queue".to_string(), + placeholder: "Select the game queue".to_string(), + options: vec![String::from("Open"), String::from("Newbie"), String::from("Normal"), String::from("Mini/Micro Theme"), String::from("Large Theme"), String::from("Other/Unknown")], + is_required: Some(true), + default_value: Some(String::from("Other/Unknown")) + }))) + + (gen_button(ButtonType::FormSubmit(FormSubmitButton { + text: "Save".to_string(), + }))) + } + } + } + .into_string(), + ) +} diff --git a/src/routes/api/dashboard/setup_data.rs b/src/routes/api/dashboard/setup_data.rs new file mode 100644 index 0000000..eaf9b66 --- /dev/null +++ b/src/routes/api/dashboard/setup_data.rs @@ -0,0 +1,49 @@ +use crate::components::{ + buttons::{gen_button, ButtonType, FormSubmitButton}, + forms::input::{gen_input, InputType, SelectMenuInput, TextInput}, +}; +use actix_web::{get, HttpResponse, Responder}; +use maud::html; + +#[get("/setup")] +async fn setup_data() -> impl Responder { + HttpResponse::Ok().body( + html! { + div."w-full h-full flex flex-col p-4" { + h1."text-3xl text-white font-bold pb-2" { "Setup Data" } + div."text-xl text-white pb-2" { "Enter the data for the setup" } + form."flex flex-col gap-2" { + label."text-xl" for="game_queue" { "Game Queue" } + (gen_input(InputType::SelectMenuInput(SelectMenuInput { + name: "game_queue".to_string(), + placeholder: "Select the game queue".to_string(), + options: vec![String::from("Open"), String::from("Newbie"), String::from("Normal"), String::from("Mini/Micro Theme"), String::from("Large Theme"), String::from("Other/Unknown")], + is_required: Some(true), + default_value: Some(String::from("Other/Unknown")) + }))) + + label."text-xl" for="game_index" { "Game Index" } + (gen_input(InputType::TextInput(TextInput { + name: "game_index".to_string(), + placeholder: "Game Index".to_string(), + is_required: Some(true), + default_value: None + }))) + + label."text-xl" for="title" { "Title" } + (gen_input(InputType::TextInput(TextInput { + name: "title".to_string(), + placeholder: "Enter the game title".to_string(), + is_required: Some(true), + default_value: None + }))) + + (gen_button(ButtonType::FormSubmit(FormSubmitButton { + text: "Save".to_string(), + }))) + } + } + } + .into_string(), + ) +} diff --git a/src/routes/api/dashboard/vote_data.rs b/src/routes/api/dashboard/vote_data.rs new file mode 100644 index 0000000..63a54c7 --- /dev/null +++ b/src/routes/api/dashboard/vote_data.rs @@ -0,0 +1,81 @@ +use crate::components::{ + buttons::{gen_button, ButtonType, FormSubmitButton}, + forms::input::{gen_input, InputType, SelectMenuInput}, +}; +use actix_web::{get, HttpResponse, Responder}; +use maud::{html, Markup}; + +struct TableRow { + name: String, + alignment: String, + role: String, + replacements: String +} +fn format_table_row(row:TableRow) -> Markup { + html!({ + tr."even:bg-zinc-600" { + 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) } + } + }) +} + + +#[get("/votes")] +async fn vote_data() -> impl Responder { + HttpResponse::Ok().body( + html! { + div."w-full h-full flex flex-col p-4" { + h1."text-3xl text-white font-bold pb-2" { "Player Data" } + div."text-xl text-white pb-2" { "Enter the data for the players in the game" } + form."flex flex-col gap-2" { + label."text-xl" for="game_queue" { "Placeholder" } + (gen_input(InputType::SelectMenuInput(SelectMenuInput { + name: "game_queue".to_string(), + placeholder: "Select the game queue".to_string(), + options: vec![String::from("Open"), String::from("Newbie"), String::from("Normal"), String::from("Mini/Micro Theme"), String::from("Large Theme"), String::from("Other/Unknown")], + is_required: Some(true), + default_value: Some(String::from("Other/Unknown")) + }))) + + (gen_button(ButtonType::FormSubmit(FormSubmitButton { + text: "Save".to_string(), + }))) + } + table."min-w-full bg-zinc-700 text-white" { + thead { + tr { + 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" { "Replacements" } + } + } + tbody id="player-table-body" { + (format_table_row(TableRow { + name: "Player 1".to_string(), + alignment: "Town".to_string(), + role: "Cop".to_string(), + replacements: "None".to_string(), + })) + (format_table_row(TableRow { + name: "Player 2".to_string(), + alignment: "Mafia".to_string(), + role: "Goon".to_string(), + replacements: "Player 4".to_string(), + })) + (format_table_row(TableRow { + name: "Player 3".to_string(), + alignment: "Town".to_string(), + role: "Cop".to_string(), + replacements: "None".to_string(), + })) + } + } + } + } + .into_string(), + ) +} diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs index ffaccc2..c4e7485 100644 --- a/src/routes/api/mod.rs +++ b/src/routes/api/mod.rs @@ -1,5 +1,7 @@ -pub mod scrape_activity_page; +pub mod dashboard; +pub mod search_or_register_thread; pub fn init(cfg: &mut actix_web::web::ServiceConfig) { - cfg.service(scrape_activity_page::scrape_activity_page); + cfg.service(actix_web::web::scope("/dashboard").configure(dashboard::init)); + cfg.service(search_or_register_thread::search_or_register_thread); } diff --git a/src/routes/api/search_or_register_thread.rs b/src/routes/api/search_or_register_thread.rs new file mode 100644 index 0000000..9f99838 --- /dev/null +++ b/src/routes/api/search_or_register_thread.rs @@ -0,0 +1,68 @@ +use crate::scraping::{ + parser::{get_search_params, get_url_from_type, PageType, PostURL, ThreadURL, URLType}, + scraper::get_page_details, +}; +use actix_web::{ + post, + web::{self, Data}, + HttpResponse, Responder, +}; +use maud::html; + +use crate::models::thread::get_thread; + +use crate::AppState; + +#[derive(serde::Deserialize)] +pub struct FormData { + url: String, +} + +#[post("/search-or-register-thread")] +async fn search_or_register_thread( + state: Data, + form: web::Form, +) -> impl Responder { + let query_search_params = get_search_params(&form.url); + let raw_url = match (query_search_params.get("t"), query_search_params.get("p")) { + (Some(thread_id), _) => get_url_from_type( + URLType::Thread(ThreadURL { + thread_id: thread_id.to_string(), + }), + PageType::Thread, + ), + (None, Some(post_id)) => get_url_from_type( + URLType::Post(PostURL { + post_id: post_id.to_string(), + }), + PageType::Thread, + ), + _ => None, + }; + + let url = match raw_url { + None => { + return HttpResponse::BadRequest().body( + html! { + div."text-red-500" { "Invalid URL" } + } + .into_string(), + ) + } + Some(url) => url, + }; + + let thread_id = match get_page_details(url.clone()).await { + Some(page) => page.thread_id, + None => None, + }; + + if let Some(t) = thread_id { + let thread: Option = get_thread(state, t.clone()).await; + println!("Found thread: {:?}", thread); + } else { + println!("No thread was found"); + } + + HttpResponse::Ok().body(html! {}.into_string()) +} diff --git a/src/routes/pages/dashboard.rs b/src/routes/pages/dashboard.rs new file mode 100644 index 0000000..cc1640b --- /dev/null +++ b/src/routes/pages/dashboard.rs @@ -0,0 +1,34 @@ +use crate::components::header::{generate_header, Header}; +use actix_web::{get, HttpResponse, Responder}; +use maud::html; + +#[get("/dashboard")] +async fn dashboard() -> impl Responder { + let header = generate_header(Header { + title: "Dashboard | MafiaScum Scraper", + }); + + let markup = html! { + (header) + body."bg-zinc-900 w-screen h-screen flex flex-row items-center justify-center text-white" { + div."bg-zinc-800 border-r border-zinc-600 shrink h-full" { + ul."w-64 flex flex-col gap-2 p-4"{ + li."cursor-pointer" hx-get="/api/dashboard/setup" hx-target="#dashboard-content" hx-trigger="click, load" { + "Setup" + } + li."cursor-pointer" hx-get="/api/dashboard/players" hx-target="#dashboard-content" { + "Players" + } + li."cursor-pointer" hx-get="/api/dashboard/votes" hx-target="#dashboard-content" { + "Votes" + } + } + } + div."grow h-full" id="dashboard-content" {} + } + }; + + let html = markup.into_string(); + + HttpResponse::Ok().body(html) +} diff --git a/src/routes/pages/mod.rs b/src/routes/pages/mod.rs index 82aca1b..c2c4a72 100644 --- a/src/routes/pages/mod.rs +++ b/src/routes/pages/mod.rs @@ -1,9 +1,9 @@ +pub mod dashboard; pub mod home; pub mod scraper; -pub mod test; pub fn init(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(home::main); - cfg.service(test::test); cfg.service(scraper::scraper); + cfg.service(dashboard::dashboard); } diff --git a/src/routes/pages/scraper.rs b/src/routes/pages/scraper.rs index c798b3d..5324f91 100644 --- a/src/routes/pages/scraper.rs +++ b/src/routes/pages/scraper.rs @@ -20,7 +20,7 @@ async fn scraper() -> impl Responder { div."text-xl text-white pb-2" { "Enter a URL to scrape from mafiascum.net" } - form."text-center w-1/2 flex flex-col items-center justify-center" hx-post="/api/scrape-activity-page" hx-target="this" hx-indicator="#scrape-form-loading" hx-swap="outerHTML" { + form."text-center w-1/2 flex flex-col items-center justify-center" hx-post="/api/search-or-register-thread" hx-target="this" hx-indicator="#scrape-form-loading" hx-swap="outerHTML" { (gen_input(InputType::TextInput(TextInput { name: "url".to_string(), placeholder: "https://mafiascum.net".to_string(), diff --git a/src/routes/pages/test.rs b/src/routes/pages/test.rs deleted file mode 100644 index f0abc8d..0000000 --- a/src/routes/pages/test.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::AppState; -use actix_web::{ - get, - web::{self, Data}, - HttpResponse, Responder, -}; -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; -use sqlx::{self, FromRow}; - -#[derive(Serialize, Deserialize, FromRow)] -pub struct User { - id: i32, - username: String, - email: String, - password_hash: String, - created_at: Option, - updated_at: Option, -} - -#[get("/test")] -async fn test(state: Data) -> impl Responder { - match sqlx::query_as!(User, "SELECT * FROM users") - .fetch_all(&state.db) - .await - { - Ok(users) => HttpResponse::Ok().json(users), - Err(_) => HttpResponse::NotFound().json("No users found"), - } -} - -#[get("/test/{id}")] -async fn test_id(state: Data, path: web::Path) -> impl Responder { - let id = path.into_inner(); - match sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) - .fetch_one(&state.db) - .await - { - Ok(user) => HttpResponse::Ok().json(user.created_at), - Err(_) => HttpResponse::NotFound().json("No user found"), - } -} diff --git a/src/scraping/parser.rs b/src/scraping/parser.rs index 32c4c84..76a8faa 100644 --- a/src/scraping/parser.rs +++ b/src/scraping/parser.rs @@ -16,19 +16,11 @@ pub enum URLType { pub enum PageType { Thread, - ActivityPage, } pub fn get_url_from_type(url_type: URLType, page_type: PageType) -> Option { let base_url = "https://forum.mafiascum.net"; match page_type { - PageType::ActivityPage => match url_type { - URLType::Thread(thread) => Some(format!( - "{}/app.php/activity_overview/{}", - base_url, thread.thread_id - )), - URLType::Post(_) => None, - }, PageType::Thread => match url_type { URLType::Thread(thread) => { Some(format!("{}/viewtopic.php?t={}", base_url, thread.thread_id)) @@ -38,23 +30,6 @@ pub fn get_url_from_type(url_type: URLType, page_type: PageType) -> Option Option { - if let Ok(parsed_url) = Url::parse(url_str) { - if let Some((_, id)) = parsed_url.query_pairs().find(|(key, _)| key == "t") { - return Some(URLType::Thread(ThreadURL { - thread_id: id.to_string(), - })); - } - - if let Some((_, id)) = parsed_url.query_pairs().find(|(key, _)| key == "p") { - return Some(URLType::Post(PostURL { - post_id: id.to_string(), - })); - } - } - None -} - pub fn get_search_params(url: &str) -> HashMap { let mut params = HashMap::new(); let base_url = "http://example.com"; // For resolving relative URLs diff --git a/src/scraping/scraper.rs b/src/scraping/scraper.rs index 794d33f..35faaab 100644 --- a/src/scraping/scraper.rs +++ b/src/scraping/scraper.rs @@ -1,6 +1,6 @@ use reqwest::Client; use select::document::Document; -use select::predicate::{Class, Name, Predicate}; +use select::predicate::Name; use crate::scraping::parser::get_search_params; @@ -51,42 +51,3 @@ pub async fn get_page_details(url: String) -> Option { Some(page_data) } - -#[derive(Debug)] -pub struct ActivityPageDetails { - pub users: Vec, -} - -pub async fn get_activity_page_details(url: String) -> Option { - let client = Client::new(); - let response = match client.get(&url).send().await { - Ok(resp) => resp, - Err(_) => return None, - }; - - let body = match response.text().await { - Ok(text) => text, - Err(_) => return None, - }; - - let document = Document::from(body.as_str()); - - let user_list_selector = Name("tbody"); - let list_node = document.find(user_list_selector).next()?; - - let mut users = Vec::new(); - - for row in list_node.find(Name("tr")) { - let mut cells = row.find(Name("td")); - if let (Some(_first_td), Some(second_td)) = (cells.next(), cells.next()) { - if let Some(username_node) = second_td - .find(Name("a").descendant(Name("span").and(Class("iso-username")))) - .next() - { - users.push(username_node.text()); - } - } - } - - Some(ActivityPageDetails { users }) -}