diff --git a/Cargo.lock b/Cargo.lock index 49f0699..aa1f594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,6 +926,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.20.10" @@ -5831,6 +5852,7 @@ name = "wrbapp" version = "0.1.0" dependencies = [ "axum", + "csv", "dioxus", "dioxus-logger", "manganis", diff --git a/Cargo.toml b/Cargo.toml index c72a5a0..bf71f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ axum = { version = "0.7.5", optional = true } once_cell = { version = "1.19.0", optional = true } surrealdb = { version = "1.5.4", features = ["kv-speedb"], optional = true } +csv = { version = "1.3.0", optional = true } + # Debug tracing = "0.1.40" dioxus-logger = "0.5.0" @@ -23,5 +25,5 @@ manganis = "0.2.2" [features] default = [] -server = [ "dioxus/axum", "tokio", "axum", "once_cell", "surrealdb" ] +server = [ "dioxus/axum", "tokio", "axum", "once_cell", "surrealdb", "csv" ] web = ["dioxus/web"] diff --git a/assets/tailwind.css b/assets/tailwind.css index 31af7e5..0f43beb 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -818,7 +818,48 @@ html { content: var(--tw-content); } +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown > *:not(summary):focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.dropdown .dropdown-content { + position: absolute; +} + +.dropdown:is(:not(details)) .dropdown-content { + visibility: hidden; + opacity: 0; + transform-origin: top; + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; +} + +.dropdown.dropdown-open .dropdown-content, +.dropdown:not(.dropdown-hover):focus .dropdown-content, +.dropdown:focus-within .dropdown-content { + visibility: visible; + opacity: 1; +} + @media (hover: hover) { + .dropdown.dropdown-hover:hover .dropdown-content { + visibility: visible; + opacity: 1; + } + .btn:hover { --tw-border-opacity: 1; border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); @@ -879,6 +920,16 @@ html { border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); } } + + .dropdown.dropdown-hover:hover .dropdown-content { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } +} + +.dropdown:is(details) summary::-webkit-details-marker { + display: none; } .input { @@ -906,6 +957,12 @@ html { margin-inline-end: -1rem; } +.join .dropdown .join-item:first-child:not(:last-child), + .join *:first-child:not(:last-child) .dropdown .join-item { + border-start-end-radius: inherit; + border-end-end-radius: inherit; +} + .navbar { display: flex; align-items: center; @@ -1024,6 +1081,14 @@ html { } } +.dropdown.dropdown-open .dropdown-content, +.dropdown:focus .dropdown-content, +.dropdown:focus-within .dropdown-content { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .input input { --tw-bg-opacity: 1; background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); diff --git a/src/util/model.rs b/src/util/model.rs index 0dd2a03..73dc5cd 100644 --- a/src/util/model.rs +++ b/src/util/model.rs @@ -1 +1,2 @@ +#[cfg(feature = "server")] pub mod member; diff --git a/src/util/model/member.rs b/src/util/model/member.rs index 1b91281..28148b7 100644 --- a/src/util/model/member.rs +++ b/src/util/model/member.rs @@ -1,15 +1,19 @@ +use std::collections::BTreeSet; + mod migration; +#[derive(Debug, PartialEq, Eq)] pub struct Member { id: String, name: Name, - hours: Vec, - groups: Vec, + hours: BTreeSet, + groups: BTreeSet, diploma: Option, registration_token: Option, } +#[derive(Debug, PartialEq, Eq)] pub struct Name { first: String, - last: String, + full: String, } diff --git a/src/util/model/member/migration.rs b/src/util/model/member/migration.rs index a2e49e7..2e65ae5 100644 --- a/src/util/model/member/migration.rs +++ b/src/util/model/member/migration.rs @@ -1,6 +1,7 @@ use super::Member; + use once_cell::sync::Lazy; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap, HashSet}; // Create a store for saving information when migrating to a new members list static MEMBERS_STORE: Lazy = Lazy::new(|| MembersStore::new()); @@ -33,10 +34,146 @@ impl MembersStore { } } -impl Member { - async fn migrate() {} +// Create a row for the csv file +#[derive(Debug, serde::Deserialize)] +struct Row { + #[serde(rename = "Relatiecode")] + id: String, + #[serde(rename = "Roepnaam")] + first_name: String, + #[serde(rename = "Tussenvoegsel(s)")] + middle_name: String, + #[serde(rename = "Achternaam")] + last_name: String, + #[serde(rename = "E-mail")] + email: String, + #[serde(rename = "Verenigingssporten")] + hours: String, + #[serde(rename = "Diploma dropdown 1")] + diploma: Option, } -fn csv_to_vec(input: String) -> Result, ()> { - Ok(vec![]) +impl Row { + fn to_member(&self) -> Member { + Member { + id: self.id.trim().to_string(), + name: super::Name { + first: self.first_name.clone(), + full: self.generate_full_name(), + }, + hours: self.generate_hours(), + groups: BTreeSet::new(), + diploma: self.diploma.clone(), + registration_token: None, + } + } + + // Get the hour data from the raw string + fn generate_hours(&self) -> BTreeSet { + let mut hours: BTreeSet = BTreeSet::new(); + + let group_parts: Vec<&str> = self.hours.split(", ").collect(); + + for group in group_parts { + let hour_parts: Vec<&str> = group.split(" - ").collect(); + + for part in hour_parts { + if &*part != "Groep" { + hours.insert(part.to_string()); + } + } + } + + hours + } + + // Generate the full name from 3 parts + fn generate_full_name(&self) -> String { + let mut parts: Vec<&str> = vec![]; + + parts.push(&self.first_name); + parts.push(&self.middle_name); + parts.push(&self.last_name); + + let filtered_strings: Vec<&str> = parts.into_iter().filter(|s| !s.is_empty()).collect(); + + filtered_strings.join(" ") + } +} + +// Convert the raw csv file to rust objects +fn csv_to_rows(input: String) -> Result, Box> { + let mut members: Vec = vec![]; + + let mut rdr = csv::Reader::from_reader(input.as_bytes()); + + for result in rdr.deserialize() { + let row: Row = result?; + members.push(row); + } + + Ok(members) +} + +// Covert the rows to formatted members +fn rows_to_members(rows: Vec) -> Vec { + let mut members: Vec = vec![]; + + for row in rows { + members.push(row.to_member()); + } + + members +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn csv_to_members_test() -> Result<(), String> { + let data = "Relatiecode,Volledige naam (1),Roepnaam,Tussenvoegsel(s),Achternaam,E-mail,2e E-mail,Verenigingssporten,Diploma dropdown 1 + D000001,\"Last, First\",First,,Last,first.last@example.com,first.last@example.nl,\"Groep - Wedstrijd - Zaterdag, Groep - Z5 - Zaterdag\",LS1 + D000002,\"Last2, First2\",First2,,Last2,first1.last@example.nl,,Groep - Z5 - Zaterdag,ZR4".to_string(); + + let expected = vec![ + Member { + id: "D000001".to_string(), + name: super::super::Name { + first: "First".to_string(), + full: "First Last".to_string(), + }, + hours: BTreeSet::from([ + "Wedstrijd".to_string(), + "Z5".to_string(), + "Zaterdag".to_string(), + ]), + groups: BTreeSet::new(), + diploma: Some("LS1".to_string()), + registration_token: None, + }, + Member { + id: "D000002".to_string(), + name: super::super::Name { + first: "First2".to_string(), + full: "First2 Last2".to_string(), + }, + hours: BTreeSet::from(["Z5".to_string(), "Zaterdag".to_string()]), + groups: BTreeSet::new(), + diploma: Some("ZR4".to_string()), + registration_token: None, + }, + ]; + + let rows = match csv_to_rows(data) { + Ok(r) => r, + Err(err) => return Err(err.to_string()), + }; + + let members = rows_to_members(rows); + + assert_eq!(expected, members); + + Ok(()) + } }