From 6ea0dfc4196c431af5d64e02bb8312dc15a021ec Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Tue, 10 Sep 2024 14:31:38 +0200 Subject: [PATCH 01/17] vingo: add season settings --- vingo/Cargo.lock | 1 + vingo/migration/Cargo.toml | 2 + .../src/m20240909_214352_create_seasons.rs | 16 ++++++- vingo/src/main.rs | 10 ++-- vingo/src/middleware.rs | 6 +-- vingo/src/routes/auth.rs | 13 ++++-- vingo/src/routes/days.rs | 3 +- vingo/src/routes/seasons.rs | 14 ++++-- vingo/src/routes/settings.rs | 46 ++++++++++++++++--- vingo/src/routes/util/session.rs | 38 +++++++++------ 10 files changed, 106 insertions(+), 43 deletions(-) diff --git a/vingo/Cargo.lock b/vingo/Cargo.lock index bf6ed25..637732e 100644 --- a/vingo/Cargo.lock +++ b/vingo/Cargo.lock @@ -1361,6 +1361,7 @@ name = "migration" version = "0.1.0" dependencies = [ "async-std", + "chrono", "sea-orm-migration", ] diff --git a/vingo/migration/Cargo.toml b/vingo/migration/Cargo.toml index 23a5d66..7108649 100644 --- a/vingo/migration/Cargo.toml +++ b/vingo/migration/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } +chrono = { version = "0.4.38", default-features = false, features = ["serde"] } [dependencies.sea-orm-migration] version = "1.0.0" @@ -18,3 +19,4 @@ features = [ "runtime-tokio-rustls", "with-chrono", ] + diff --git a/vingo/migration/src/m20240909_214352_create_seasons.rs b/vingo/migration/src/m20240909_214352_create_seasons.rs index bc8f153..04856a4 100644 --- a/vingo/migration/src/m20240909_214352_create_seasons.rs +++ b/vingo/migration/src/m20240909_214352_create_seasons.rs @@ -1,4 +1,5 @@ use sea_orm_migration::{prelude::*, schema::*}; +use chrono::NaiveDate; #[derive(DeriveMigrationName)] pub struct Migration; @@ -25,7 +26,18 @@ impl MigrationTrait for Migration { .col(date(Season::End)) .to_owned(), ) - .await + .await?; + + // insert the first season (which is all data) + let insert = Query::insert() + .into_table(Season::Table) + .columns([Season::Name, Season::Start, Season::End]) + .values_panic(["All".into(), NaiveDate::MIN.into(), NaiveDate::MAX.into()]) + .to_owned(); + + manager.exec_stmt(insert).await?; + + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { @@ -33,4 +45,4 @@ impl MigrationTrait for Migration { .drop_table(Table::drop().table(Season::Table).to_owned()) .await } -} \ No newline at end of file +} diff --git a/vingo/src/main.rs b/vingo/src/main.rs index 2493af7..1a793d0 100644 --- a/vingo/src/main.rs +++ b/vingo/src/main.rs @@ -101,18 +101,16 @@ fn authenticated_routes() -> Router { ) .route("/scans", get(scans::get_for_current_user)) .route("/leaderboard", get(leaderboard::get)) - .route("/settings/seasons/:season_id", post(settings::set_season)) + .route("/seasons", get(seasons::get)) + .route("/settings", get(settings::get).patch(settings::update)) .route_layer(from_fn(middleware::is_logged_in)) } fn admin_routes() -> Router { Router::new() - .route( - "/days", - get(days::get).post(days::add_multiple), - ) + .route("/days", get(days::get).post(days::add_multiple)) .route("/days/:day_id", delete(days::delete)) - .route("/seasons", get(seasons::get).post(seasons::add)) + .route("/seasons", post(seasons::add)) .route("/seasons/:season_id", delete(seasons::delete)) .route_layer(from_fn(middleware::is_admin)) } diff --git a/vingo/src/middleware.rs b/vingo/src/middleware.rs index 234a4e2..fa233d1 100644 --- a/vingo/src/middleware.rs +++ b/vingo/src/middleware.rs @@ -1,8 +1,4 @@ -use axum::{ - extract::Request, - middleware::Next, - response::{IntoResponse, Response}, -}; +use axum::{extract::Request, middleware::Next, response::IntoResponse}; use reqwest::StatusCode; use tower_sessions::Session; diff --git a/vingo/src/routes/auth.rs b/vingo/src/routes/auth.rs index c5b9a62..20244ee 100644 --- a/vingo/src/routes/auth.rs +++ b/vingo/src/routes/auth.rs @@ -5,7 +5,7 @@ use chrono::Local; use rand::distributions::{Alphanumeric, DistString}; use reqwest::StatusCode; use sea_orm::sea_query::OnConflict; -use sea_orm::{EntityTrait, Set, TryIntoModel}; +use sea_orm::{EntityTrait, Set}; use serde::{Deserialize, Serialize}; use tower_sessions::Session; use user::Model; @@ -133,10 +133,13 @@ pub async fn callback( .or_log((StatusCode::INTERNAL_SERVER_ERROR, "user insert error"))?; session.clear().await; - session.insert(SessionKeys::User.as_str(), db_user).await.or_log(( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to insert user in session", - ))?; + session + .insert(SessionKeys::User.as_str(), db_user) + .await + .or_log(( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to insert user in session", + ))?; Ok(Redirect::to(FRONTEND_URL)) } diff --git a/vingo/src/routes/days.rs b/vingo/src/routes/days.rs index ccb5547..e71a827 100644 --- a/vingo/src/routes/days.rs +++ b/vingo/src/routes/days.rs @@ -2,7 +2,7 @@ use axum::{ extract::{Path, State}, Json, }; -use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeDelta, Weekday}; +use chrono::{Datelike, NaiveDate, TimeDelta, Weekday}; use reqwest::StatusCode; use sea_orm::{ActiveModelTrait, EntityTrait, Set, TransactionTrait}; use serde::{Deserialize, Serialize}; @@ -38,7 +38,6 @@ pub async fn add_multiple( let mut current_date = day_range.start_date; while current_date <= day_range.end_date { - if current_date.weekday() == Weekday::Sat || current_date.weekday() == Weekday::Sun { current_date += TimeDelta::days(1); continue; diff --git a/vingo/src/routes/seasons.rs b/vingo/src/routes/seasons.rs index 6c29830..b7cb141 100644 --- a/vingo/src/routes/seasons.rs +++ b/vingo/src/routes/seasons.rs @@ -2,9 +2,9 @@ use axum::{ extract::{Path, State}, Json, }; -use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeDelta, Weekday}; +use chrono::NaiveDate; use reqwest::StatusCode; -use sea_orm::{ActiveModelTrait, EntityTrait, Set, TransactionTrait}; +use sea_orm::{ActiveModelTrait, EntityTrait, Set}; use serde::{Deserialize, Serialize}; use crate::{ @@ -27,13 +27,19 @@ pub struct SeasonAddBody { start: NaiveDate, end: NaiveDate, } -pub async fn add(state: State, Json(new_season): Json) -> ResponseResult<()> { +pub async fn add( + state: State, + Json(new_season): Json, +) -> ResponseResult<()> { season::ActiveModel { name: Set(new_season.name), start: Set(new_season.start), end: Set(new_season.end), ..Default::default() - }.insert(&state.db).await.or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to insert season"))?; + } + .insert(&state.db) + .await + .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to insert season"))?; Ok(()) } diff --git a/vingo/src/routes/settings.rs b/vingo/src/routes/settings.rs index cdfb67b..f8a80c6 100644 --- a/vingo/src/routes/settings.rs +++ b/vingo/src/routes/settings.rs @@ -1,17 +1,51 @@ -use axum::extract::{Path, State}; +use axum::{extract::State, Json}; use reqwest::StatusCode; +use sea_orm::EntityTrait; +use serde::{Deserialize, Serialize}; use tower_sessions::Session; use crate::{ - entities::{prelude::*, *}, + entities::prelude::*, AppState, }; -use super::util::{errors::{ResponseResult, ResultAndLogError}, session::{self, SessionKeys}}; +use super::util::{ + errors::{ResponseResult, ResultAndLogError}, + session::{get_season, SessionKeys}, +}; +#[derive(Debug, Serialize, Deserialize)] +pub struct SettingsGetBody { + season: i32, +} +pub async fn get(session: Session, state: State) -> ResponseResult> { + let season = get_season(&session, &state).await?; + Ok(Json(SettingsGetBody { season: season.id })) +} -pub async fn set_season(state: State, session: Session, Path(season_id): Path) -> ResponseResult<()> { - session.insert(SessionKeys::Season.as_str(), season_id).await.or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to insert season into session"))?; +#[derive(Debug, Serialize, Deserialize)] +pub struct SettingsUpdateBody { + season: Option, +} +pub async fn update( + session: Session, + state: State, + Json(settings): Json, +) -> ResponseResult<()> { + if let Some(season_id) = settings.season { + let season = Season::find_by_id(season_id) + .one(&state.db) + .await + .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to get season"))? + .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "no season 0"))?; + session + .insert(SessionKeys::Season.as_str(), season) + .await + .or_log(( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to insert season into session", + ))?; + } Ok(()) -} \ No newline at end of file +} diff --git a/vingo/src/routes/util/session.rs b/vingo/src/routes/util/session.rs index 10b8d0d..5f54c80 100644 --- a/vingo/src/routes/util/session.rs +++ b/vingo/src/routes/util/session.rs @@ -1,13 +1,16 @@ +use axum::extract::State; use reqwest::StatusCode; +use sea_orm::EntityTrait; use tower_sessions::Session; -use user::Model; use super::errors::{ResponseResult, ResultAndLogError}; -use crate::entities::*; - +use crate::{ + entities::{prelude::*, *}, + AppState, +}; pub enum SessionKeys { User, - Season, + Season, } impl SessionKeys { @@ -19,8 +22,7 @@ impl SessionKeys { } } - -pub async fn get_user(session: &Session) -> ResponseResult { +pub async fn get_user(session: &Session) -> ResponseResult { session .get(SessionKeys::User.as_str()) .await @@ -28,13 +30,23 @@ pub async fn get_user(session: &Session) -> ResponseResult { .ok_or((StatusCode::UNAUTHORIZED, "Not logged in")) } -pub type SeasonId = i32; - -pub async fn get_season(session: &Session) -> ResponseResult { - Ok(session +pub async fn get_season( + session: &Session, + state: &State, +) -> ResponseResult { + let season: Option = session .get(SessionKeys::Season.as_str()) .await - .or_log((StatusCode::INTERNAL_SERVER_ERROR, "Failed to get session"))? - .or(Some(0)) // set season to 0 (all) if none is set - .expect("can't be none")) + .or_log((StatusCode::INTERNAL_SERVER_ERROR, "Failed to get session"))?; + + let season_or_default = match season { + Some(season_model) => season_model, + None => Season::find_by_id(0) + .one(&state.db) + .await + .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to get season"))? + .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "no season 0"))?, + }; + + Ok(season_or_default) } From bc857a1076f7e40b0f49723a8c65a192f206e652 Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Tue, 10 Sep 2024 20:55:38 +0200 Subject: [PATCH 02/17] vingo: filter scans --- vingo/src/routes/scans.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vingo/src/routes/scans.rs b/vingo/src/routes/scans.rs index 84a3b34..5336296 100644 --- a/vingo/src/routes/scans.rs +++ b/vingo/src/routes/scans.rs @@ -14,7 +14,7 @@ use tower_sessions::Session; use super::util::{ errors::{ResponseResult, ResultAndLogError}, - session::get_user, + session::{get_season, get_user}, }; const SCAN_KEY: &str = "bad_key"; @@ -24,10 +24,14 @@ pub async fn get_for_current_user( state: State, ) -> ResponseResult>> { let user = get_user(&session).await?; + let season = get_season(&session, &state).await?; let scans = Scan::find() .join(InnerJoin, scan::Relation::Card.def()) .join(InnerJoin, card::Relation::User.def()) .filter(user::Column::Id.eq(user.id)) + // scan time > start && scan_time < end + .filter(scan::Column::ScanTime.gte(season.start)) + .filter(scan::Column::ScanTime.lte(season.end)) .all(&state.db) .await .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to get scans"))?; From 5fd9b2114083851c83901de5aa3715f79926183d Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Wed, 11 Sep 2024 21:42:04 +0200 Subject: [PATCH 03/17] vingo: make the all season in migrations --- vingo/migration/src/m20240909_214352_create_seasons.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vingo/migration/src/m20240909_214352_create_seasons.rs b/vingo/migration/src/m20240909_214352_create_seasons.rs index 04856a4..47799e3 100644 --- a/vingo/migration/src/m20240909_214352_create_seasons.rs +++ b/vingo/migration/src/m20240909_214352_create_seasons.rs @@ -32,7 +32,7 @@ impl MigrationTrait for Migration { let insert = Query::insert() .into_table(Season::Table) .columns([Season::Name, Season::Start, Season::End]) - .values_panic(["All".into(), NaiveDate::MIN.into(), NaiveDate::MAX.into()]) + .values_panic(["All".into(), NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().into(), NaiveDate::from_ymd_opt(3000, 1, 1).unwrap().into()]) .to_owned(); manager.exec_stmt(insert).await?; From a21167b0fa70ed928983db4c5acfe9d46940cba2 Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Wed, 11 Sep 2024 21:56:26 +0200 Subject: [PATCH 04/17] vingo: postgres id is from 1 --- vingo/src/routes/util/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vingo/src/routes/util/session.rs b/vingo/src/routes/util/session.rs index 5f54c80..8c1b511 100644 --- a/vingo/src/routes/util/session.rs +++ b/vingo/src/routes/util/session.rs @@ -41,11 +41,11 @@ pub async fn get_season( let season_or_default = match season { Some(season_model) => season_model, - None => Season::find_by_id(0) + None => Season::find_by_id(1) .one(&state.db) .await .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to get season"))? - .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "no season 0"))?, + .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "no season 1"))?, }; Ok(season_or_default) From 4f7e1a5367b3d490109cd358b4b8282a44847e28 Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Thu, 12 Sep 2024 01:12:54 +0200 Subject: [PATCH 05/17] vingo: env to login as dummy account --- dev.sh | 8 ++++---- docker-compose.yml | 1 + vingo/dev.env | 1 + vingo/src/routes/util/session.rs | 10 ++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 vingo/dev.env diff --git a/dev.sh b/dev.sh index 44278d8..abc37ea 100755 --- a/dev.sh +++ b/dev.sh @@ -21,7 +21,7 @@ done # Build the docker containers if clean flag is set if [ "$clean" = true ]; then -# rm vingo/.env || true + rm vingo/.env || true rm vinvoor/.env || true docker compose -f docker-compose.yml build fi @@ -29,9 +29,9 @@ fi # Check for the required files -#if [ ! -f vingo/.env ]; then -# cp vingo/dev.env vingo/.env -#fi +if [ ! -f vingo/.env ]; then + cp vingo/dev.env vingo/.env +fi if [ ! -f vinvoor/.env ]; then cp vinvoor/dev.env vinvoor/.env fi diff --git a/docker-compose.yml b/docker-compose.yml index 4cb3511..72a92d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: - "host.docker.internal:host-gateway" zess-backend: + env_file: "vingo/.env" build: context: vingo dockerfile: Dockerfile.dev diff --git a/vingo/dev.env b/vingo/dev.env new file mode 100644 index 0000000..89f05e6 --- /dev/null +++ b/vingo/dev.env @@ -0,0 +1 @@ +DEBUG_LOGIN="TRUE" diff --git a/vingo/src/routes/util/session.rs b/vingo/src/routes/util/session.rs index 8c1b511..815447a 100644 --- a/vingo/src/routes/util/session.rs +++ b/vingo/src/routes/util/session.rs @@ -1,4 +1,7 @@ +use std::{env, sync::LazyLock}; + use axum::extract::State; +use chrono::Local; use reqwest::StatusCode; use sea_orm::EntityTrait; use tower_sessions::Session; @@ -8,6 +11,8 @@ use crate::{ entities::{prelude::*, *}, AppState, }; + +static DEBUG_LOGIN: LazyLock = LazyLock::new(|| env::var("DEBUG_LOGIN").unwrap_or("".into()) == "TRUE"); pub enum SessionKeys { User, Season, @@ -23,6 +28,11 @@ impl SessionKeys { } pub async fn get_user(session: &Session) -> ResponseResult { + // act as always logged in + if *DEBUG_LOGIN { + return Ok(user::Model { id: 1, name: "vincentest".into(), admin: true, created_at: Local::now().fixed_offset() }); + } + session .get(SessionKeys::User.as_str()) .await From 602494063b4edbd184e4faf7069fcf196600d6d3 Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Thu, 12 Sep 2024 01:56:30 +0200 Subject: [PATCH 06/17] vingo: is_current field to seasons --- vingo/src/routes/seasons.rs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/vingo/src/routes/seasons.rs b/vingo/src/routes/seasons.rs index b7cb141..e07ceed 100644 --- a/vingo/src/routes/seasons.rs +++ b/vingo/src/routes/seasons.rs @@ -3,8 +3,10 @@ use axum::{ Json, }; use chrono::NaiveDate; + use reqwest::StatusCode; -use sea_orm::{ActiveModelTrait, EntityTrait, Set}; +use sea_orm::{sea_query::Expr, ColumnTrait}; +use sea_orm::{ActiveModelTrait, EntityTrait, FromQueryResult, QuerySelect, Set}; use serde::{Deserialize, Serialize}; use crate::{ @@ -14,11 +16,30 @@ use crate::{ use super::util::errors::{ResponseResult, ResultAndLogError}; -pub async fn get(state: State) -> ResponseResult>> { - Ok(Json(Season::find().all(&state.db).await.or_log(( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to get seasons", - ))?)) +#[derive(Debug, FromQueryResult, Serialize, Deserialize)] +pub struct SeasonGet { + id: i32, + name: String, + start: NaiveDate, + end: NaiveDate, + is_current: bool, +} + +pub async fn get(state: State) -> ResponseResult>> { + Ok(Json( + Season::find() + .column_as( + Expr::col(season::Column::Start) + .lte(Expr::current_date()) + .and(Expr::col(season::Column::End).gte(Expr::current_date())) + .and(Expr::col(season::Column::Id).ne(1)), + "is_current", + ) + .into_model::() + .all(&state.db) + .await + .or_log((StatusCode::INTERNAL_SERVER_ERROR, "failed to get seasons"))?, + )) } #[derive(Debug, Serialize, Deserialize)] From e8098620adc1045cc83f75ea181e31add6fd1de4 Mon Sep 17 00:00:00 2001 From: Hannes Klinckaert Date: Thu, 12 Sep 2024 01:57:29 +0200 Subject: [PATCH 07/17] vingo: fix warnings --- .../src/m20240909_214352_create_seasons.rs | 16 ++++++++++------ vingo/src/routes/seasons.rs | 2 +- vingo/src/routes/settings.rs | 10 +++++----- vingo/src/routes/util/session.rs | 10 ++++++++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/vingo/migration/src/m20240909_214352_create_seasons.rs b/vingo/migration/src/m20240909_214352_create_seasons.rs index 47799e3..c1dbaea 100644 --- a/vingo/migration/src/m20240909_214352_create_seasons.rs +++ b/vingo/migration/src/m20240909_214352_create_seasons.rs @@ -1,5 +1,5 @@ -use sea_orm_migration::{prelude::*, schema::*}; use chrono::NaiveDate; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -27,13 +27,17 @@ impl MigrationTrait for Migration { .to_owned(), ) .await?; - + // insert the first season (which is all data) let insert = Query::insert() - .into_table(Season::Table) - .columns([Season::Name, Season::Start, Season::End]) - .values_panic(["All".into(), NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().into(), NaiveDate::from_ymd_opt(3000, 1, 1).unwrap().into()]) - .to_owned(); + .into_table(Season::Table) + .columns([Season::Name, Season::Start, Season::End]) + .values_panic([ + "All".into(), + NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().into(), + NaiveDate::from_ymd_opt(3000, 1, 1).unwrap().into(), + ]) + .to_owned(); manager.exec_stmt(insert).await?; diff --git a/vingo/src/routes/seasons.rs b/vingo/src/routes/seasons.rs index e07ceed..beb9351 100644 --- a/vingo/src/routes/seasons.rs +++ b/vingo/src/routes/seasons.rs @@ -5,7 +5,7 @@ use axum::{ use chrono::NaiveDate; use reqwest::StatusCode; -use sea_orm::{sea_query::Expr, ColumnTrait}; +use sea_orm::sea_query::Expr; use sea_orm::{ActiveModelTrait, EntityTrait, FromQueryResult, QuerySelect, Set}; use serde::{Deserialize, Serialize}; diff --git a/vingo/src/routes/settings.rs b/vingo/src/routes/settings.rs index f8a80c6..32b1710 100644 --- a/vingo/src/routes/settings.rs +++ b/vingo/src/routes/settings.rs @@ -4,10 +4,7 @@ use sea_orm::EntityTrait; use serde::{Deserialize, Serialize}; use tower_sessions::Session; -use crate::{ - entities::prelude::*, - AppState, -}; +use crate::{entities::prelude::*, AppState}; use super::util::{ errors::{ResponseResult, ResultAndLogError}, @@ -18,7 +15,10 @@ use super::util::{ pub struct SettingsGetBody { season: i32, } -pub async fn get(session: Session, state: State) -> ResponseResult> { +pub async fn get( + session: Session, + state: State, +) -> ResponseResult> { let season = get_season(&session, &state).await?; Ok(Json(SettingsGetBody { season: season.id })) } diff --git a/vingo/src/routes/util/session.rs b/vingo/src/routes/util/session.rs index 815447a..f558bce 100644 --- a/vingo/src/routes/util/session.rs +++ b/vingo/src/routes/util/session.rs @@ -12,7 +12,8 @@ use crate::{ AppState, }; -static DEBUG_LOGIN: LazyLock = LazyLock::new(|| env::var("DEBUG_LOGIN").unwrap_or("".into()) == "TRUE"); +static DEBUG_LOGIN: LazyLock = + LazyLock::new(|| env::var("DEBUG_LOGIN").unwrap_or("".into()) == "TRUE"); pub enum SessionKeys { User, Season, @@ -30,7 +31,12 @@ impl SessionKeys { pub async fn get_user(session: &Session) -> ResponseResult { // act as always logged in if *DEBUG_LOGIN { - return Ok(user::Model { id: 1, name: "vincentest".into(), admin: true, created_at: Local::now().fixed_offset() }); + return Ok(user::Model { + id: 1, + name: "vincentest".into(), + admin: true, + created_at: Local::now().fixed_offset(), + }); } session From c8efb22b3db3c1a0a347e35e0ca6f3adc3990444 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Thu, 12 Sep 2024 00:22:22 +0200 Subject: [PATCH 08/17] vinvoor: add season admin settings --- vinvoor/src/hooks/admin/useAdminDays.ts | 27 ++++ vinvoor/src/hooks/admin/useAdminSeason.ts | 20 +++ vinvoor/src/hooks/useDays.ts | 18 --- vinvoor/src/hooks/useSeasons.ts | 13 ++ vinvoor/src/hooks/useSettings.ts | 5 +- vinvoor/src/settings/admin/Admin.tsx | 4 + vinvoor/src/settings/admin/days/Days.tsx | 4 +- vinvoor/src/settings/admin/days/DaysAdd.tsx | 41 ++++--- vinvoor/src/settings/admin/days/DaysTable.tsx | 9 +- .../src/settings/admin/days/DaysTableBody.tsx | 11 +- .../settings/admin/days/DaysTableToolbar.tsx | 4 +- .../src/settings/admin/seasons/Seasons.tsx | 27 ++++ .../src/settings/admin/seasons/SeasonsAdd.tsx | 96 +++++++++++++++ .../settings/admin/seasons/SeasonsTable.tsx | 116 ++++++++++++++++++ .../admin/seasons/SeasonsTableBody.tsx | 81 ++++++++++++ .../admin/seasons/SeasonsTableHead.tsx | 61 +++++++++ vinvoor/src/types/days.ts | 2 + vinvoor/src/types/settings.ts | 23 +--- 18 files changed, 494 insertions(+), 68 deletions(-) create mode 100644 vinvoor/src/hooks/admin/useAdminDays.ts create mode 100644 vinvoor/src/hooks/admin/useAdminSeason.ts delete mode 100644 vinvoor/src/hooks/useDays.ts create mode 100644 vinvoor/src/hooks/useSeasons.ts create mode 100644 vinvoor/src/settings/admin/seasons/Seasons.tsx create mode 100644 vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx create mode 100644 vinvoor/src/settings/admin/seasons/SeasonsTable.tsx create mode 100644 vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx create mode 100644 vinvoor/src/settings/admin/seasons/SeasonsTableHead.tsx diff --git a/vinvoor/src/hooks/admin/useAdminDays.ts b/vinvoor/src/hooks/admin/useAdminDays.ts new file mode 100644 index 0000000..2f0afa7 --- /dev/null +++ b/vinvoor/src/hooks/admin/useAdminDays.ts @@ -0,0 +1,27 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { convertDayJSON, Day, DayJSON } from "../../types/days"; +import { deleteAPI, getApi, postApi } from "../../util/fetch"; +import { Dayjs } from "dayjs"; + +const ENDPOINT = "admin/days"; + +export const useAdminDays = () => + useQuery({ + queryKey: ["adminDays"], + queryFn: () => getApi(ENDPOINT, convertDayJSON), + retry: 1, + }); + +export const useAdminAddDay = () => + useMutation({ + mutationFn: (args: { startDate: Dayjs; endDate: Dayjs }) => + postApi(ENDPOINT, { + start_date: args.startDate.format("YYYY-MM-DD"), + end_date: args.endDate.format("YYYY-MM-DD"), + }), + }); + +export const useAdminDeleteDay = () => + useMutation({ + mutationFn: (id: number) => deleteAPI(`${ENDPOINT}/${id}`), + }); diff --git a/vinvoor/src/hooks/admin/useAdminSeason.ts b/vinvoor/src/hooks/admin/useAdminSeason.ts new file mode 100644 index 0000000..db90542 --- /dev/null +++ b/vinvoor/src/hooks/admin/useAdminSeason.ts @@ -0,0 +1,20 @@ +import { useMutation } from "@tanstack/react-query"; +import { deleteAPI, postApi } from "../../util/fetch"; +import { Dayjs } from "dayjs"; + +const ENDPOINT = "admin/seasons"; + +export const useAdminAddSeason = () => + useMutation({ + mutationFn: (args: { name: string; startDate: Dayjs; endDate: Dayjs }) => + postApi(ENDPOINT, { + name: args.name, + start: args.startDate.format("YYYY-MM-DD"), + end: args.endDate.format("YYYY-MM-DD"), + }), + }); + +export const useAdminDeleteSeason = () => + useMutation({ + mutationFn: (id: number) => deleteAPI(`${ENDPOINT}/${id}`), + }); diff --git a/vinvoor/src/hooks/useDays.ts b/vinvoor/src/hooks/useDays.ts deleted file mode 100644 index 9df3290..0000000 --- a/vinvoor/src/hooks/useDays.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { convertDayJSON, Day, DayJSON } from "../types/days"; -import { deleteAPI, getApi } from "../util/fetch"; - -const ENDPOINT = "admin/days"; - -export const useDays = () => - useQuery({ - queryKey: ["days"], - queryFn: () => getApi(ENDPOINT, convertDayJSON), - retry: 1, - }); - -export const useDeleteDay = () => { - return useMutation({ - mutationFn: (id: number) => deleteAPI(`${ENDPOINT}/${id}`), - }); -}; diff --git a/vinvoor/src/hooks/useSeasons.ts b/vinvoor/src/hooks/useSeasons.ts new file mode 100644 index 0000000..cce92fe --- /dev/null +++ b/vinvoor/src/hooks/useSeasons.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { getApi } from "../util/fetch"; +import { convertSeasonJSON, Season, SeasonJSON } from "../types/seasons"; + +const ENDPOINT = "seasons"; + +export const useSeasons = () => { + return useQuery({ + queryKey: ["seasons"], + queryFn: () => getApi(ENDPOINT, convertSeasonJSON), + retry: 1, + }); +}; diff --git a/vinvoor/src/hooks/useSettings.ts b/vinvoor/src/hooks/useSettings.ts index d621d49..d8ba304 100644 --- a/vinvoor/src/hooks/useSettings.ts +++ b/vinvoor/src/hooks/useSettings.ts @@ -11,8 +11,8 @@ export const useSettings = () => retry: 1, }); -export const usePatchSettings = () => { - return useMutation({ +export const usePatchSettings = () => + useMutation({ mutationFn: (args: { scanInOut: boolean; leaderboard: boolean; @@ -24,4 +24,3 @@ export const usePatchSettings = () => { public: args.public, }), }); -}; diff --git a/vinvoor/src/settings/admin/Admin.tsx b/vinvoor/src/settings/admin/Admin.tsx index 33b7155..2242cfe 100644 --- a/vinvoor/src/settings/admin/Admin.tsx +++ b/vinvoor/src/settings/admin/Admin.tsx @@ -1,6 +1,7 @@ import { Alert, Grid, Typography } from "@mui/material"; import { FC } from "react"; import { Days } from "./days/Days"; +import { Seasons } from "./seasons/Seasons"; export const Admin: FC = () => { return ( @@ -25,6 +26,9 @@ export const Admin: FC = () => { + + + ); }; diff --git a/vinvoor/src/settings/admin/days/Days.tsx b/vinvoor/src/settings/admin/days/Days.tsx index 5aa058f..e06703e 100644 --- a/vinvoor/src/settings/admin/days/Days.tsx +++ b/vinvoor/src/settings/admin/days/Days.tsx @@ -2,10 +2,10 @@ import { Grid } from "@mui/material"; import { LoadingSkeleton } from "../../../components/LoadingSkeleton"; import { DaysAdd } from "./DaysAdd"; import { DaysTable } from "./DaysTable"; -import { useDays } from "../../../hooks/useDays"; +import { useAdminDays } from "../../../hooks/admin/useAdminDays"; export const Days = () => { - const daysQuery = useDays(); + const daysQuery = useAdminDays(); return ( diff --git a/vinvoor/src/settings/admin/days/DaysAdd.tsx b/vinvoor/src/settings/admin/days/DaysAdd.tsx index b6b2727..26000e2 100644 --- a/vinvoor/src/settings/admin/days/DaysAdd.tsx +++ b/vinvoor/src/settings/admin/days/DaysAdd.tsx @@ -5,11 +5,14 @@ import dayjs, { Dayjs } from "dayjs"; import { useSnackbar } from "notistack"; import { Dispatch, SetStateAction, useState } from "react"; import { TypographyG } from "../../../components/TypographyG"; -import { postApi } from "../../../util/fetch"; -import { useDays } from "../../../hooks/useDays"; +import { + useAdminAddDay, + useAdminDays, +} from "../../../hooks/admin/useAdminDays"; export const DaysAdd = () => { - const { refetch } = useDays(); + const { refetch } = useAdminDays(); + const addDay = useAdminAddDay(); const [startDate, setStartDate] = useState(dayjs()); const [endDate, setEndDate] = useState(dayjs()); @@ -28,22 +31,22 @@ export const DaysAdd = () => { return; } - postApi("admin/days", { - start_date: startDate.format("YYYY-MM-DD"), - end_date: endDate.format("YYYY-MM-DD"), - }) - .then(() => { - enqueueSnackbar("successfully saved days", { - variant: "success", - }); - void refetch(); - }) - .catch(error => - // This is the admin page so just show the error - enqueueSnackbar(`Failed to save days: ${error}`, { - variant: "error", - }), - ); + addDay.mutate( + { startDate, endDate }, + { + onSuccess: () => { + enqueueSnackbar("successfully saved days", { + variant: "success", + }); + void refetch(); + }, + onError: error => + // This is the admin page so just show the error + enqueueSnackbar(`Failed to save days: ${error.message}`, { + variant: "error", + }), + }, + ); }; return ( diff --git a/vinvoor/src/settings/admin/days/DaysTable.tsx b/vinvoor/src/settings/admin/days/DaysTable.tsx index ff94a1a..0fd03d4 100644 --- a/vinvoor/src/settings/admin/days/DaysTable.tsx +++ b/vinvoor/src/settings/admin/days/DaysTable.tsx @@ -8,13 +8,16 @@ import { randomInt } from "../../../util/util"; import { DaysTableBody } from "./DaysTableBody"; import { DaysTableHead } from "./DaysTableHead"; import { DaysTableToolbar } from "./DaysTableToolbar"; -import { useDays, useDeleteDay } from "../../../hooks/useDays"; +import { + useAdminDays, + useAdminDeleteDay, +} from "../../../hooks/admin/useAdminDays"; export const DaysTable = () => { - const { data: days, refetch } = useDays(); + const { data: days, refetch } = useAdminDays(); if (!days) return null; // Can never happen - const deleteDay = useDeleteDay(); + const deleteDay = useAdminDeleteDay(); const [rows, setRows] = useState(days); const [selected, setSelected] = useState([]); const [deleting, setDeleting] = useState(false); diff --git a/vinvoor/src/settings/admin/days/DaysTableBody.tsx b/vinvoor/src/settings/admin/days/DaysTableBody.tsx index eff25c9..b83de5a 100644 --- a/vinvoor/src/settings/admin/days/DaysTableBody.tsx +++ b/vinvoor/src/settings/admin/days/DaysTableBody.tsx @@ -10,7 +10,10 @@ import { import { useSnackbar } from "notistack"; import { FC, ReactNode } from "react"; import { Day, daysHeadCells } from "../../../types/days"; -import { useDays, useDeleteDay } from "../../../hooks/useDays"; +import { + useAdminDays, + useAdminDeleteDay, +} from "../../../hooks/admin/useAdminDays"; interface DaysTableBodyProps { rows: readonly Day[]; @@ -25,15 +28,15 @@ export const DaysTableBody: FC = ({ isSelected, deleting, }) => { - const { refetch } = useDays(); - const deleteCard = useDeleteDay(); + const { refetch } = useAdminDays(); + const deleteDay = useAdminDeleteDay(); const { enqueueSnackbar } = useSnackbar(); const handleClick = (id: number) => { if (isSelected(id)) handleSelect(id); // This will remove it from the selected list - deleteCard.mutate(id, { + deleteDay.mutate(id, { onSuccess: () => { enqueueSnackbar("Deleted streakday", { variant: "success" }); void refetch(); diff --git a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx index d89534a..4436677 100644 --- a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx +++ b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx @@ -4,7 +4,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; import { ChangeEvent, Dispatch, FC, SetStateAction, useState } from "react"; import { Optional } from "../../../types/general"; -import { useDays } from "../../../hooks/useDays"; +import { useAdminDays } from "../../../hooks/admin/useAdminDays"; interface DaysTableToolbarProps { dateFilter: [Optional, Optional]; @@ -23,7 +23,7 @@ export const DaysTableToolbar: FC = ({ weekendsFilter, setWeekendsFilter, }) => { - const { data: days } = useDays(); + const { data: days } = useAdminDays(); if (!days) return null; // Can never happen const [startDate, setStartDate] = useState( diff --git a/vinvoor/src/settings/admin/seasons/Seasons.tsx b/vinvoor/src/settings/admin/seasons/Seasons.tsx new file mode 100644 index 0000000..09d1458 --- /dev/null +++ b/vinvoor/src/settings/admin/seasons/Seasons.tsx @@ -0,0 +1,27 @@ +import { Grid } from "@mui/material"; +import { LoadingSkeleton } from "../../../components/LoadingSkeleton"; +import { useSeasons } from "../../../hooks/useSeasons"; +import { SeasonsTable } from "./SeasonsTable"; +import { SeasonsAdd } from "./SeasonsAdd"; + +export const Seasons = () => { + const seasonsQuery = useSeasons(); + + return ( + + + + + + + + + + + ); +}; diff --git a/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx b/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx new file mode 100644 index 0000000..1238de7 --- /dev/null +++ b/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx @@ -0,0 +1,96 @@ +import dayjs, { Dayjs } from "dayjs"; +import { useAdminAddSeason } from "../../../hooks/admin/useAdminSeason"; +import { useSeasons } from "../../../hooks/useSeasons"; +import { Dispatch, SetStateAction, useState } from "react"; +import { useSnackbar } from "notistack"; +import { Box, Button, Paper, Stack, TextField } from "@mui/material"; +import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { TypographyG } from "../../../components/TypographyG"; + +export const SeasonsAdd = () => { + const { refetch } = useSeasons(); + const addSeason = useAdminAddSeason(); + const [startDate, setStartDate] = useState(dayjs()); + const [endDate, setEndDate] = useState(dayjs()); + const [name, setName] = useState(""); + + const { enqueueSnackbar } = useSnackbar(); + + const handleDateChange = ( + date: Dayjs | null, + setter: Dispatch>, + ) => setter(date); + + const handleNameChange = (event: React.ChangeEvent) => + setName(event.target.value); + + const handleOnClick = () => { + if (!startDate || !endDate) { + enqueueSnackbar("Please select a start and end date", { + variant: "error", + }); + return; + } + + addSeason.mutate( + { name, startDate, endDate }, + { + onSuccess: () => { + enqueueSnackbar("successfully saved season", { + variant: "success", + }); + void refetch(); + }, + onError: error => + // This is the admin page so just show the error + enqueueSnackbar(`Failed to save seasib: ${error.message}`, { + variant: "error", + }), + }, + ); + }; + + return ( + + + Add Season + + + handleDateChange(newValue, setStartDate)} + /> + handleDateChange(newValue, setEndDate)} + /> + + + + + + + + + ); +}; diff --git a/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx b/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx new file mode 100644 index 0000000..afb8313 --- /dev/null +++ b/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx @@ -0,0 +1,116 @@ +import { Paper, Stack, Table, TableContainer } from "@mui/material"; +import { TypographyG } from "../../../components/TypographyG"; +import { ChangeEvent, useState } from "react"; +import { randomInt } from "../../../util/util"; +import { useSnackbar } from "notistack"; +import { useAdminDeleteSeason } from "../../../hooks/admin/useAdminSeason"; +import { useSeasons } from "../../../hooks/useSeasons"; +import { SeasonsTableHead } from "./SeasonsTableHead"; +import { SeasonsTableBody } from "./SeasonsTableBody"; + +export const SeasonsTable = () => { + const { data: seasons, refetch } = useSeasons(); + if (!seasons) return null; // Can never happen + + const deleteSeason = useAdminDeleteSeason(); + const [selected, setSelected] = useState([]); + const [deleting, setDeleting] = useState(false); + + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + const handleDelete = () => { + setDeleting(true); + const key = randomInt(); + enqueueSnackbar("Deleting...", { + variant: "info", + key: key, + persist: true, + }); + + const promises = selected.map(id => + deleteSeason.mutate(id, { + onError: (error: Error) => + enqueueSnackbar(`Failed to delete season ${id}: ${error.message}`, { + variant: "error", + }), + }), + ); + + void Promise.all(promises) + .then(() => { + closeSnackbar(key); + enqueueSnackbar( + `Deleted ${selected.length} season${selected.length > 1 ? "s" : ""}`, + { + variant: "success", + }, + ); + + setSelected([]); + setDeleting(false); + }) + .finally(() => void refetch()); + }; + + const handleSelect = (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + switch (selectedIndex) { + case -1: + newSelected = newSelected.concat(selected, id); + break; + case 0: + newSelected = newSelected.concat(selected.slice(1)); + break; + case selected.length - 1: + newSelected = newSelected.concat(selected.slice(0, -1)); + break; + default: + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1), + ); + } + + setSelected(newSelected); + }; + + const handleSelectAll = (event: ChangeEvent) => { + if (event.target.checked) setSelected(seasons.map(season => season.id)); + else setSelected([]); + }; + + const isSelected = (id: number) => selected.includes(id); + + return ( + + + Edit Seasons + + + + + +
+
+
+
+
+ ); +}; diff --git a/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx b/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx new file mode 100644 index 0000000..0f029fc --- /dev/null +++ b/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx @@ -0,0 +1,81 @@ +import { + Checkbox, + IconButton, + TableBody, + TableCell, + TableRow, + Typography, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { FC, ReactNode } from "react"; +import { useSeasons } from "../../../hooks/useSeasons"; +import { seasonsHeadCells } from "../../../types/seasons"; +import { useSnackbar } from "notistack"; +import { useAdminDeleteSeason } from "../../../hooks/admin/useAdminSeason"; + +interface SeasonsTableBodyProps { + handleSelect: (id: number) => void; + isSelected: (id: number) => boolean; + deleting: boolean; +} + +export const SeasonsTableBody: FC = ({ + handleSelect, + isSelected, + deleting, +}) => { + const { data: seasons, refetch } = useSeasons(); + if (!seasons) return null; // Can never happen + + const deleteSeason = useAdminDeleteSeason(); + const { enqueueSnackbar } = useSnackbar(); + + const handleClick = (id: number) => { + console.log("Hi"); + if (isSelected(id)) handleSelect(id); // This will remove it from the selected list + + deleteSeason.mutate(id, { + onSuccess: () => { + enqueueSnackbar("Deleted season", { variant: "success" }); + void refetch(); + }, + onError: (error: Error) => + enqueueSnackbar(`Failed to delete season ${id}: ${error.message}`, { + variant: "error", + }), + }); + }; + + return ( + + {seasons.map(season => ( + + handleSelect(season.id)}> + + + {seasonsHeadCells.map(headCell => ( + + + {headCell.convert + ? headCell.convert(season[headCell.id]) + : (season[headCell.id] as ReactNode)} + + + ))} + + handleClick(season.id)} + > + + + + + ))} + + ); +}; diff --git a/vinvoor/src/settings/admin/seasons/SeasonsTableHead.tsx b/vinvoor/src/settings/admin/seasons/SeasonsTableHead.tsx new file mode 100644 index 0000000..91c40b5 --- /dev/null +++ b/vinvoor/src/settings/admin/seasons/SeasonsTableHead.tsx @@ -0,0 +1,61 @@ +import { + Box, + Button, + Checkbox, + TableCell, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { ChangeEvent, FC } from "react"; +import { seasonsHeadCells } from "../../../types/seasons"; + +interface SeasonsTableHeadProps { + rowCount: number; + numSelected: number; + onSelectAll: (event: ChangeEvent) => void; + handleDelete: () => void; + deleting: boolean; +} + +export const SeasonsTableHead: FC = ({ + rowCount, + numSelected, + onSelectAll, + handleDelete, + deleting, +}) => { + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAll} + /> + + {seasonsHeadCells.map(headCell => ( + + {headCell.label} + + ))} + + + + + + + + ); +}; diff --git a/vinvoor/src/types/days.ts b/vinvoor/src/types/days.ts index dd28134..6824549 100644 --- a/vinvoor/src/types/days.ts +++ b/vinvoor/src/types/days.ts @@ -22,6 +22,8 @@ export const convertDayJSON = (daysJSON: DayJSON[]): Day[] => })) .sort((a, b) => a.date.getTime() - b.date.getTime()); +// Table + export const daysHeadCells: readonly TableHeadCell[] = [ { id: "date", diff --git a/vinvoor/src/types/settings.ts b/vinvoor/src/types/settings.ts index 64e6075..0a0da50 100644 --- a/vinvoor/src/types/settings.ts +++ b/vinvoor/src/types/settings.ts @@ -16,7 +16,7 @@ export const converSettingsJSON = (settingsJSON: SettingsJSON): Settings => ({ ...settingsJSON, }); -// // Table +// Table interface AdjustableSettings { id: keyof Settings; @@ -25,20 +25,9 @@ interface AdjustableSettings { } export const adjustableSettings: AdjustableSettings[] = [ - // { - // id: "scanInOut", - // name: "Scan in and out", - // description: - // "A second scan on the same day will be interpreted as a scan out", - // }, - // { - // id: "leaderboard", - // name: "Leaderboard", - // description: "Show yourself on the leaderboard", - // }, - // { - // id: "public", - // name: "Public", - // description: "Let others see you!", - // }, + { + id: "season", + name: "Selected season", + description: "The season you are currently viewing", + }, ]; From 79bea64509b23610e07e2cb0657c7b397a24d55b Mon Sep 17 00:00:00 2001 From: Topvennie Date: Thu, 12 Sep 2024 13:53:51 +0200 Subject: [PATCH 09/17] vinvoor: implement seasons --- vinvoor/src/hooks/admin/useAdminSeason.ts | 13 ++- vinvoor/src/hooks/useSeasons.ts | 19 +++- vinvoor/src/hooks/useSettings.ts | 12 +-- vinvoor/src/navbar/NavBar.tsx | 7 ++ vinvoor/src/navbar/NavBarSeasons.tsx | 92 +++++++++++++++++++ .../src/settings/admin/seasons/Seasons.tsx | 4 +- .../src/settings/admin/seasons/SeasonsAdd.tsx | 8 +- .../settings/admin/seasons/SeasonsTable.tsx | 8 +- .../admin/seasons/SeasonsTableBody.tsx | 10 +- vinvoor/src/types/seasons.ts | 30 +++++- 10 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 vinvoor/src/navbar/NavBarSeasons.tsx diff --git a/vinvoor/src/hooks/admin/useAdminSeason.ts b/vinvoor/src/hooks/admin/useAdminSeason.ts index db90542..b9fe6d2 100644 --- a/vinvoor/src/hooks/admin/useAdminSeason.ts +++ b/vinvoor/src/hooks/admin/useAdminSeason.ts @@ -1,9 +1,18 @@ -import { useMutation } from "@tanstack/react-query"; -import { deleteAPI, postApi } from "../../util/fetch"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { deleteAPI, getApi, postApi } from "../../util/fetch"; import { Dayjs } from "dayjs"; +import { convertSeasonJSON, Season, SeasonJSON } from "../../types/seasons"; const ENDPOINT = "admin/seasons"; +export const useAdminSeasons = () => { + return useQuery({ + queryKey: ["adminSeasons"], + queryFn: () => getApi("seasons", convertSeasonJSON), + retry: 1, + }); +}; + export const useAdminAddSeason = () => useMutation({ mutationFn: (args: { name: string; startDate: Dayjs; endDate: Dayjs }) => diff --git a/vinvoor/src/hooks/useSeasons.ts b/vinvoor/src/hooks/useSeasons.ts index cce92fe..dca85db 100644 --- a/vinvoor/src/hooks/useSeasons.ts +++ b/vinvoor/src/hooks/useSeasons.ts @@ -1,6 +1,7 @@ -import { useQuery } from "@tanstack/react-query"; +import { MutateOptions, useQuery } from "@tanstack/react-query"; import { getApi } from "../util/fetch"; import { convertSeasonJSON, Season, SeasonJSON } from "../types/seasons"; +import { usePatchSettings } from "./useSettings"; const ENDPOINT = "seasons"; @@ -11,3 +12,19 @@ export const useSeasons = () => { retry: 1, }); }; + +export const useSetSeason = () => { + const { mutate, ...rest } = usePatchSettings(); + + const setSeason = ( + id: number, + options: MutateOptions< + unknown, + Error, + Record, + unknown + >, + ) => mutate({ season: id }, options); + + return { setSeason, ...rest }; +}; diff --git a/vinvoor/src/hooks/useSettings.ts b/vinvoor/src/hooks/useSettings.ts index d8ba304..dcf1e10 100644 --- a/vinvoor/src/hooks/useSettings.ts +++ b/vinvoor/src/hooks/useSettings.ts @@ -13,14 +13,6 @@ export const useSettings = () => export const usePatchSettings = () => useMutation({ - mutationFn: (args: { - scanInOut: boolean; - leaderboard: boolean; - public: boolean; - }) => - patchApi(ENDPOINT, { - scanInOut: args.scanInOut, - leaderboard: args.leaderboard, - public: args.public, - }), + mutationFn: (args: Record) => + patchApi(ENDPOINT, args), }); diff --git a/vinvoor/src/navbar/NavBar.tsx b/vinvoor/src/navbar/NavBar.tsx index 0f80a0f..8479413 100644 --- a/vinvoor/src/navbar/NavBar.tsx +++ b/vinvoor/src/navbar/NavBar.tsx @@ -12,6 +12,7 @@ import { NavBarPages } from "./NavBarPages"; import { NavBarSandwich } from "./NavBarSandwich"; import { NavBarUserMenu } from "./NavBarUserMenu"; import { useUser } from "../hooks/useUser"; +import { NavBarSeasons } from "./NavBarSeasons"; export interface PageIcon { page: string; @@ -75,6 +76,12 @@ export const NavBar = () => { + {/* Display a season selector */} + + + + + {/* Display a dark mode switch and the user menu */} diff --git a/vinvoor/src/navbar/NavBarSeasons.tsx b/vinvoor/src/navbar/NavBarSeasons.tsx new file mode 100644 index 0000000..85c776b --- /dev/null +++ b/vinvoor/src/navbar/NavBarSeasons.tsx @@ -0,0 +1,92 @@ +import { useState, MouseEvent } from "react"; +import { useSeasons, useSetSeason } from "../hooks/useSeasons"; +import { useSettings } from "../hooks/useSettings"; +import { Button, IconButton, Menu, MenuItem, Typography } from "@mui/material"; +import { ArrowDropDown, Refresh } from "@mui/icons-material"; + +export const NavBarSeasons = () => { + const { data: seasons } = useSeasons(); + const { data: settings, refetch } = useSettings(); + const { setSeason } = useSetSeason(); + + const currentSeason = seasons?.find(season => season.isCurrent)?.id ?? -1; + + const [anchorElUser, setAnchorElUser] = useState( + undefined, + ); + + const handleOpenUserMenu = (event: MouseEvent) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(undefined); + }; + + const handleClickSeason = (id: number) => { + setSeason(id, { onSuccess: () => void refetch() }); + handleCloseUserMenu(); + }; + + const handleResetSeason = () => + setSeason(currentSeason, { + onSuccess: () => void refetch(), + }); + + return ( + <> + {seasons && settings && ( + <> + + + + + + {seasons.map(season => ( + handleClickSeason(season.id)} + > + {season.name} + + ))} + + + )} + + ); +}; diff --git a/vinvoor/src/settings/admin/seasons/Seasons.tsx b/vinvoor/src/settings/admin/seasons/Seasons.tsx index 09d1458..5b4af5b 100644 --- a/vinvoor/src/settings/admin/seasons/Seasons.tsx +++ b/vinvoor/src/settings/admin/seasons/Seasons.tsx @@ -1,11 +1,11 @@ import { Grid } from "@mui/material"; import { LoadingSkeleton } from "../../../components/LoadingSkeleton"; -import { useSeasons } from "../../../hooks/useSeasons"; import { SeasonsTable } from "./SeasonsTable"; import { SeasonsAdd } from "./SeasonsAdd"; +import { useAdminSeasons } from "../../../hooks/admin/useAdminSeason"; export const Seasons = () => { - const seasonsQuery = useSeasons(); + const seasonsQuery = useAdminSeasons(); return ( diff --git a/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx b/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx index 1238de7..f308d4a 100644 --- a/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx +++ b/vinvoor/src/settings/admin/seasons/SeasonsAdd.tsx @@ -1,6 +1,8 @@ import dayjs, { Dayjs } from "dayjs"; -import { useAdminAddSeason } from "../../../hooks/admin/useAdminSeason"; -import { useSeasons } from "../../../hooks/useSeasons"; +import { + useAdminAddSeason, + useAdminSeasons, +} from "../../../hooks/admin/useAdminSeason"; import { Dispatch, SetStateAction, useState } from "react"; import { useSnackbar } from "notistack"; import { Box, Button, Paper, Stack, TextField } from "@mui/material"; @@ -9,7 +11,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { TypographyG } from "../../../components/TypographyG"; export const SeasonsAdd = () => { - const { refetch } = useSeasons(); + const { refetch } = useAdminSeasons(); const addSeason = useAdminAddSeason(); const [startDate, setStartDate] = useState(dayjs()); const [endDate, setEndDate] = useState(dayjs()); diff --git a/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx b/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx index afb8313..1963ad3 100644 --- a/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx +++ b/vinvoor/src/settings/admin/seasons/SeasonsTable.tsx @@ -3,13 +3,15 @@ import { TypographyG } from "../../../components/TypographyG"; import { ChangeEvent, useState } from "react"; import { randomInt } from "../../../util/util"; import { useSnackbar } from "notistack"; -import { useAdminDeleteSeason } from "../../../hooks/admin/useAdminSeason"; -import { useSeasons } from "../../../hooks/useSeasons"; +import { + useAdminDeleteSeason, + useAdminSeasons, +} from "../../../hooks/admin/useAdminSeason"; import { SeasonsTableHead } from "./SeasonsTableHead"; import { SeasonsTableBody } from "./SeasonsTableBody"; export const SeasonsTable = () => { - const { data: seasons, refetch } = useSeasons(); + const { data: seasons, refetch } = useAdminSeasons(); if (!seasons) return null; // Can never happen const deleteSeason = useAdminDeleteSeason(); diff --git a/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx b/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx index 0f029fc..0409a96 100644 --- a/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx +++ b/vinvoor/src/settings/admin/seasons/SeasonsTableBody.tsx @@ -8,10 +8,12 @@ import { } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import { FC, ReactNode } from "react"; -import { useSeasons } from "../../../hooks/useSeasons"; -import { seasonsHeadCells } from "../../../types/seasons"; import { useSnackbar } from "notistack"; -import { useAdminDeleteSeason } from "../../../hooks/admin/useAdminSeason"; +import { + useAdminDeleteSeason, + useAdminSeasons, +} from "../../../hooks/admin/useAdminSeason"; +import { seasonsHeadCells } from "../../../types/seasons"; interface SeasonsTableBodyProps { handleSelect: (id: number) => void; @@ -24,7 +26,7 @@ export const SeasonsTableBody: FC = ({ isSelected, deleting, }) => { - const { data: seasons, refetch } = useSeasons(); + const { data: seasons, refetch } = useAdminSeasons(); if (!seasons) return null; // Can never happen const deleteSeason = useAdminDeleteSeason(); diff --git a/vinvoor/src/types/seasons.ts b/vinvoor/src/types/seasons.ts index fba1d43..a1baf91 100644 --- a/vinvoor/src/types/seasons.ts +++ b/vinvoor/src/types/seasons.ts @@ -1,4 +1,4 @@ -import { Base, BaseJSON } from "./general"; +import { Base, BaseJSON, TableHeadCell } from "./general"; // External @@ -6,6 +6,7 @@ export interface SeasonJSON extends BaseJSON { name: string; start: string; end: string; + is_current: boolean; } // Internal @@ -14,6 +15,7 @@ export interface Season extends Base { name: string; start: Date; end: Date; + isCurrent: boolean; } // Converters @@ -23,4 +25,30 @@ export const convertSeasonJSON = (seasonsJSON: SeasonJSON[]): Season[] => ...seasonJSON, start: new Date(seasonJSON.start), end: new Date(seasonJSON.end), + isCurrent: seasonJSON.is_current, })); + +// Table + +export const seasonsHeadCells: readonly TableHeadCell[] = [ + { + id: "name", + label: "Name", + align: "left", + padding: "normal", + }, + { + id: "start", + label: "Start Date", + align: "right", + padding: "normal", + convert: (value: Date) => value.toDateString(), + } as TableHeadCell, + { + id: "end", + label: "End Date", + align: "right", + padding: "normal", + convert: (value: Date) => value.toDateString(), + } as TableHeadCell, +]; From c0934847200b812a9856913ede9f6a5f56eda742 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Thu, 12 Sep 2024 14:25:21 +0200 Subject: [PATCH 10/17] vinvoor: add season filter to admin days --- vinvoor/src/settings/admin/Admin.tsx | 4 +- vinvoor/src/settings/admin/days/Days.tsx | 8 +-- vinvoor/src/settings/admin/days/DaysTable.tsx | 16 +++++- .../settings/admin/days/DaysTableToolbar.tsx | 53 +++++++++++++++++-- .../src/settings/admin/seasons/Seasons.tsx | 10 ++-- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/vinvoor/src/settings/admin/Admin.tsx b/vinvoor/src/settings/admin/Admin.tsx index 2242cfe..246665b 100644 --- a/vinvoor/src/settings/admin/Admin.tsx +++ b/vinvoor/src/settings/admin/Admin.tsx @@ -23,10 +23,10 @@ export const Admin: FC = () => { - + - + diff --git a/vinvoor/src/settings/admin/days/Days.tsx b/vinvoor/src/settings/admin/days/Days.tsx index e06703e..b495276 100644 --- a/vinvoor/src/settings/admin/days/Days.tsx +++ b/vinvoor/src/settings/admin/days/Days.tsx @@ -15,12 +15,12 @@ export const Days = () => { columnSpacing={4} rowSpacing={6} > - - - - + + + + ); diff --git a/vinvoor/src/settings/admin/days/DaysTable.tsx b/vinvoor/src/settings/admin/days/DaysTable.tsx index 0fd03d4..42e3667 100644 --- a/vinvoor/src/settings/admin/days/DaysTable.tsx +++ b/vinvoor/src/settings/admin/days/DaysTable.tsx @@ -12,9 +12,11 @@ import { useAdminDays, useAdminDeleteDay, } from "../../../hooks/admin/useAdminDays"; +import { useAdminSeasons } from "../../../hooks/admin/useAdminSeason"; export const DaysTable = () => { const { data: days, refetch } = useAdminDays(); + const { data: seasons } = useAdminSeasons(); if (!days) return null; // Can never happen const deleteDay = useAdminDeleteDay(); @@ -25,6 +27,8 @@ export const DaysTable = () => { const [dateFilter, setDateFilter] = useState< [Optional, Optional] >([undefined, undefined]); + const [seasonsFilter, setSeasonsFilter] = + useState>(undefined); const [weekdaysFilter, setWeekdaysFilter] = useState(false); const [weekendsFilter, setWeekendsFilter] = useState(false); @@ -39,6 +43,14 @@ export const DaysTable = () => { day.date.getTime() <= dateFilter[1]!.getTime(), ); } + if (seasonsFilter) { + const season = seasons?.find(season => season.id === seasonsFilter); + if (season) { + filteredDays = filteredDays.filter( + day => day.date >= season.start && day.date <= season.end, + ); + } + } if (weekdaysFilter) { filteredDays = filteredDays.filter( day => day.date.getDay() !== 0 && day.date.getDay() !== 6, @@ -123,7 +135,7 @@ export const DaysTable = () => { useEffect( () => setRows(filterDays()), - [days, dateFilter, weekdaysFilter, weekendsFilter], + [days, dateFilter, seasonsFilter, weekdaysFilter, weekendsFilter], ); return ( @@ -139,6 +151,8 @@ export const DaysTable = () => { {...{ dateFilter, setDateFilter, + seasonsFilter, + setSeasonsFilter, weekdaysFilter, setWeekdaysFilter, weekendsFilter, diff --git a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx index 4436677..9cb1340 100644 --- a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx +++ b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx @@ -1,14 +1,24 @@ -import { Checkbox, Stack, Typography } from "@mui/material"; +import { + Checkbox, + MenuItem, + Select, + SelectChangeEvent, + Stack, + Typography, +} from "@mui/material"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; import { ChangeEvent, Dispatch, FC, SetStateAction, useState } from "react"; import { Optional } from "../../../types/general"; import { useAdminDays } from "../../../hooks/admin/useAdminDays"; +import { useAdminSeasons } from "../../../hooks/admin/useAdminSeason"; interface DaysTableToolbarProps { dateFilter: [Optional, Optional]; setDateFilter: Dispatch, Optional]>>; + seasonsFilter: Optional; + setSeasonsFilter: Dispatch>>; weekdaysFilter: boolean; setWeekdaysFilter: Dispatch>; weekendsFilter: boolean; @@ -18,13 +28,16 @@ interface DaysTableToolbarProps { export const DaysTableToolbar: FC = ({ dateFilter, setDateFilter, + seasonsFilter: seasonFilter, + setSeasonsFilter: setSeasonFilter, weekdaysFilter, setWeekdaysFilter, weekendsFilter, setWeekendsFilter, }) => { const { data: days } = useAdminDays(); - if (!days) return null; // Can never happen + const { data: seasons } = useAdminSeasons(); + if (!days || !seasons) return null; // Can never happen const [startDate, setStartDate] = useState( days.length ? dayjs(days[0].date) : dayjs(), @@ -33,6 +46,9 @@ export const DaysTableToolbar: FC = ({ days.length ? dayjs(days[days.length - 1].date) : dayjs(), ); + const [selectedSeason, setSelectedSeason] = + useState>(seasonFilter); + const handleDateChange = ( date: Dayjs | null, setter: Dispatch>, @@ -53,13 +69,21 @@ export const DaysTableToolbar: FC = ({ else setDateFilter([undefined, undefined]); }; + const handleSeasonChange = (event: SelectChangeEvent) => + setSelectedSeason(Number(event.target.value)); + + const handleClickSeason = (event: ChangeEvent) => { + if (event.target.checked) setSeasonFilter(selectedSeason); + else setSeasonFilter(undefined); + }; + const handleClickBoolean = ( event: ChangeEvent, setter: Dispatch>, ) => setter(event.target.checked); return ( - + = ({ /> + + + Filter season + + { container justifyContent="space-between" columnSpacing={4} - rowSpacing={4} + rowSpacing={6} > - - - - + + + +
); From b8520e62674b850af2eb752630c805412f708949 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Thu, 12 Sep 2024 14:47:46 +0200 Subject: [PATCH 11/17] vinvoor: readd settings page --- vinvoor/src/navbar/NavBar.tsx | 2 +- vinvoor/src/settings/Settings.tsx | 94 +++++++++---------- .../settings/admin/days/DaysTableToolbar.tsx | 11 ++- vinvoor/src/types/settings.ts | 16 ---- 4 files changed, 52 insertions(+), 71 deletions(-) diff --git a/vinvoor/src/navbar/NavBar.tsx b/vinvoor/src/navbar/NavBar.tsx index 8479413..26febee 100644 --- a/vinvoor/src/navbar/NavBar.tsx +++ b/vinvoor/src/navbar/NavBar.tsx @@ -26,7 +26,7 @@ const navBarPages: PageIcon[] = [ ]; const userMenuPages: PageIcon[] = [ - // { page: "Settings", icon: }, + { page: "Settings", icon: }, ]; export const NavBar = () => { diff --git a/vinvoor/src/settings/Settings.tsx b/vinvoor/src/settings/Settings.tsx index a071227..3d30ad2 100644 --- a/vinvoor/src/settings/Settings.tsx +++ b/vinvoor/src/settings/Settings.tsx @@ -1,21 +1,21 @@ -import { ChangeEvent, useState } from "react"; +import { useEffect, useState } from "react"; import { usePatchSettings, useSettings } from "../hooks/useSettings"; import { useSnackbar } from "notistack"; import { useConfirm } from "material-ui-confirm"; import { Box, Button, - Checkbox, - FormControl, - FormControlLabel, Grid, + MenuItem, Paper, + Select, + SelectChangeEvent, Stack, Tooltip, Typography, } from "@mui/material"; -import { adjustableSettings } from "../types/settings"; import HelpCircleOutline from "mdi-material-ui/HelpCircleOutline"; +import { useSeasons } from "../hooks/useSeasons"; const saveSuccess = "Settings saved successfully"; const saveFailure = "Unable to save settings"; @@ -32,35 +32,28 @@ const handleDeleteContent = ( export const Settings = () => { const { data: settingsTruth, refetch } = useSettings(); - if (!settingsTruth) return null; // Can never happen + const { data: seasons } = useSeasons(); + if (!settingsTruth || !seasons) return null; // Can never happen const patchSettings = usePatchSettings(); const [settings, setSettings] = useState({ ...settingsTruth }); const { enqueueSnackbar } = useSnackbar(); const confirm = useConfirm(); - const handleChange = (event: ChangeEvent) => { + const handleSeasonChange = (event: SelectChangeEvent) => setSettings({ ...settings, - [event.target.name]: event.target.checked, + season: parseInt(event.target.value), }); - }; const handleSubmit = () => { - patchSettings.mutate( - { - scanInOut: settings.scanInOut, - leaderboard: settings.leaderboard, - public: settings.public, - }, - { - onSuccess: () => { - enqueueSnackbar(saveSuccess, { variant: "success" }); - void refetch(); - }, - onError: () => enqueueSnackbar(saveFailure, { variant: "error" }), + patchSettings.mutate(settings, { + onSuccess: () => { + enqueueSnackbar(saveSuccess, { variant: "success" }); + void refetch(); }, - ); + onError: () => enqueueSnackbar(saveFailure, { variant: "error" }), + }); }; const handleDelete = () => { @@ -81,6 +74,10 @@ export const Settings = () => { }); }; + useEffect(() => { + setSettings({ ...settingsTruth, season: settingsTruth.season }); + }, [settingsTruth.season]); + return ( { > - - {adjustableSettings.map(setting => ( - - } - label={ - - {setting.name} - - - - - } - key={setting.id} + + + - ))} - + + Select season: + + diff --git a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx index 9cb1340..fa0f17d 100644 --- a/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx +++ b/vinvoor/src/settings/admin/days/DaysTableToolbar.tsx @@ -70,7 +70,7 @@ export const DaysTableToolbar: FC = ({ }; const handleSeasonChange = (event: SelectChangeEvent) => - setSelectedSeason(Number(event.target.value)); + setSelectedSeason(parseInt(event.target.value)); const handleClickSeason = (event: ChangeEvent) => { if (event.target.checked) setSeasonFilter(selectedSeason); @@ -116,10 +116,11 @@ export const DaysTableToolbar: FC = ({ Filter season